geoai 1.0.4 → 1.0.6

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 CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.6] - 2026-07-16
11
+
12
+ ### Added
13
+ - ChangeStar ViT-B building footprint model (`geobase/changestar-building-segmentation-vitb`)
14
+ - Building segmentation factory to route Hub `modelId` to the correct runner
15
+
16
+ ### Changed
17
+ - `building-footprint-segmentation` now defaults to ChangeStar; pass
18
+ `modelId: "geobase/building-footprint-segmentation"` for the lighter model
19
+
20
+ ## [1.0.5] - 2026-07-15
21
+
22
+ ### Added
23
+ - WMS map provider with OGC GetMap support (EPSG:3857 bbox, WMS 1.1.1 / 1.3.0)
24
+ - `examples/06-wms-quickstart` — NRW orthophotos (`nw_dop_rgb`), draw-to-detect, NRW geocoder
25
+ - Interactive TMS and WMS providers in `live-examples-nextjs` task demos
26
+ - WMS documentation (`wms.mdx`), map-providers table update, and 17 WMS tests (including pipeline inference)
27
+ - Project roadmap (`_docs4devs/ROADMAP.md`) for use-case guides and quantization pipeline backlog
28
+
10
29
  ## [1.0.4] - 2026-07-11
11
30
 
12
31
  ### 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.3/geoai.js"></script>
43
+ <script src="https://unpkg.com/geoai@1.0.6/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.3/geoai.min.js"></script>
48
+ <script src="https://cdn.jsdelivr.net/npm/geoai@1.0.6/geoai.min.js"></script>
49
49
  ```
50
50
 
51
51
  When using CDN, the library is available globally as `geoai`.
@@ -235,6 +235,7 @@ const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
235
235
 
236
236
  - **Documentation**: [docs.geobase.app/geoai](https://docs.geobase.app/geoai) - Comprehensive documentation, examples, and API reference
237
237
  - **Live Examples**: [docs.geobase.app/geoai-live](https://docs.geobase.app/geoai-live) - Interactive examples and demos
238
+ - **Roadmap**: [_docs4devs/ROADMAP.md](_docs4devs/ROADMAP.md) - Planned docs, quantization pipeline, and related backlog
238
239
  - **Community**: [GitHub Discussions](https://github.com/decision-labs/geoai.js/discussions) - Ask questions, share ideas, and connect with other developers
239
240
  - **Code**: [GitHub Repository](https://github.com/decision-labs/geoai.js) - Source code and contributions
240
241
  - **Issues**: [GitHub Issues](https://github.com/decision-labs/geoai.js/issues) - Report bugs and request features
@@ -247,4 +248,4 @@ We welcome contributions! Please see our [contributing guide](CONTRIBUTING.md) f
247
248
 
248
249
  MIT License - see [LICENSE.md](LICENSE.md) for details.
249
250
 
250
- [//]: <> (Toggle CI on)
251
+ [//]: <> (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;
393
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
394
394
  if (instance.model_id !== model_id) {
395
395
  return true;
396
396
  }
@@ -419,6 +419,12 @@ const parametersChanged = (instance, model_id, providerParams, modelParams) => {
419
419
  if (((_h = instance.providerParams) == null ? void 0 : _h.baseUrl) !== (providerParams == null ? void 0 : providerParams.baseUrl)) {
420
420
  return true;
421
421
  }
422
+ break;
423
+ case "wms":
424
+ if (((_i = instance.providerParams) == null ? void 0 : _i.baseUrl) !== (providerParams == null ? void 0 : providerParams.baseUrl) || ((_j = instance.providerParams) == null ? void 0 : _j.layers) !== (providerParams == null ? void 0 : providerParams.layers) || ((_k = instance.providerParams) == null ? void 0 : _k.version) !== (providerParams == null ? void 0 : providerParams.version) || ((_l = instance.providerParams) == null ? void 0 : _l.crs) !== (providerParams == null ? void 0 : providerParams.crs)) {
425
+ return true;
426
+ }
427
+ break;
422
428
  }
423
429
  if (modelParams) {
424
430
  const instanceModelParams = instance.modelParams || {};
@@ -1283,6 +1289,97 @@ class Tms extends MapSource {
1283
1289
  return url;
1284
1290
  }
1285
1291
  }
1292
+ function formatWmsBbox(bbox2, crs, version) {
1293
+ const [minX, minY, maxX, maxY] = bbox2;
1294
+ if (version === "1.3.0" && crs === "EPSG:4326") {
1295
+ return `${minY},${minX},${maxY},${maxX}`;
1296
+ }
1297
+ return `${minX},${minY},${maxX},${maxY}`;
1298
+ }
1299
+ function getTileBbox(tileCoords, crs, tileSize) {
1300
+ const [x, y, z] = tileCoords;
1301
+ if (crs === "EPSG:3857") {
1302
+ const toMeters = pixelsToMeters;
1303
+ const min = toMeters([x * tileSize, y * tileSize, z], tileSize);
1304
+ const max = toMeters([(x + 1) * tileSize, (y + 1) * tileSize, z], tileSize);
1305
+ return [
1306
+ Math.min(min[0], max[0]),
1307
+ Math.min(min[1], max[1]),
1308
+ Math.max(min[0], max[0]),
1309
+ Math.max(min[1], max[1])
1310
+ ];
1311
+ }
1312
+ return tileToBBox(tileCoords);
1313
+ }
1314
+ function buildWmsGetMapUrl(config, tileCoords) {
1315
+ const version = config.version ?? "1.1.1";
1316
+ const crs = config.crs ?? "EPSG:3857";
1317
+ const tileSize = config.tileSize ?? 256;
1318
+ const bbox2 = getTileBbox(tileCoords, crs, tileSize);
1319
+ const bboxParam = formatWmsBbox(bbox2, crs, version);
1320
+ const params = {
1321
+ SERVICE: "WMS",
1322
+ REQUEST: "GetMap",
1323
+ VERSION: version,
1324
+ LAYERS: config.layers,
1325
+ STYLES: config.styles ?? "",
1326
+ FORMAT: config.format ?? "image/png",
1327
+ WIDTH: tileSize.toString(),
1328
+ HEIGHT: tileSize.toString(),
1329
+ BBOX: bboxParam,
1330
+ ...version === "1.3.0" ? { CRS: crs } : { SRS: crs },
1331
+ ...config.transparent ? { TRANSPARENT: "TRUE" } : {},
1332
+ ...config.extraParams ?? {}
1333
+ };
1334
+ const url = new URL(config.baseUrl);
1335
+ for (const [key, value] of Object.entries(params)) {
1336
+ url.searchParams.set(key, value);
1337
+ }
1338
+ return url.toString();
1339
+ }
1340
+ class Wms extends MapSource {
1341
+ constructor(config) {
1342
+ super();
1343
+ __publicField(this, "baseUrl");
1344
+ __publicField(this, "layers");
1345
+ __publicField(this, "version");
1346
+ __publicField(this, "crs");
1347
+ __publicField(this, "format");
1348
+ __publicField(this, "styles");
1349
+ __publicField(this, "transparent");
1350
+ __publicField(this, "attribution");
1351
+ __publicField(this, "tileSize");
1352
+ __publicField(this, "headers");
1353
+ __publicField(this, "extraParams");
1354
+ this.baseUrl = config.baseUrl;
1355
+ this.layers = config.layers;
1356
+ this.version = config.version ?? "1.1.1";
1357
+ this.crs = config.crs ?? "EPSG:3857";
1358
+ this.format = config.format ?? "image/png";
1359
+ this.styles = config.styles ?? "";
1360
+ this.transparent = config.transparent ?? false;
1361
+ this.attribution = config.attribution ?? "WMS Provider";
1362
+ this.tileSize = config.tileSize ?? 256;
1363
+ this.headers = config.headers;
1364
+ this.extraParams = config.extraParams;
1365
+ }
1366
+ getTileUrlFromTileCoords(tileCoords, instance) {
1367
+ return buildWmsGetMapUrl(
1368
+ {
1369
+ baseUrl: instance.baseUrl,
1370
+ layers: instance.layers,
1371
+ version: instance.version,
1372
+ crs: instance.crs,
1373
+ format: instance.format,
1374
+ styles: instance.styles,
1375
+ transparent: instance.transparent,
1376
+ tileSize: instance.tileSize,
1377
+ extraParams: instance.extraParams
1378
+ },
1379
+ tileCoords
1380
+ );
1381
+ }
1382
+ }
1286
1383
  class BaseModel {
1287
1384
  constructor(model_id, providerParams, modelParams) {
1288
1385
  __publicField(this, "providerParams");
@@ -1337,6 +1434,21 @@ class BaseModel {
1337
1434
  scheme: this.providerParams.scheme
1338
1435
  });
1339
1436
  break;
1437
+ case "wms":
1438
+ this.dataProvider = new Wms({
1439
+ baseUrl: this.providerParams.baseUrl,
1440
+ layers: this.providerParams.layers,
1441
+ version: this.providerParams.version,
1442
+ crs: this.providerParams.crs,
1443
+ format: this.providerParams.format,
1444
+ styles: this.providerParams.styles,
1445
+ transparent: this.providerParams.transparent,
1446
+ attribution: this.providerParams.attribution,
1447
+ tileSize: this.providerParams.tileSize,
1448
+ headers: this.providerParams.headers,
1449
+ extraParams: this.providerParams.extraParams
1450
+ });
1451
+ break;
1340
1452
  case "sentinel":
1341
1453
  throw new Error("Sentinel provider not implemented yet");
1342
1454
  default:
@@ -2997,6 +3109,377 @@ const _OilStorageTankDetection = class _OilStorageTankDetection extends BaseMode
2997
3109
  };
2998
3110
  __publicField(_OilStorageTankDetection, "instance", null);
2999
3111
  let OilStorageTankDetection = _OilStorageTankDetection;
3112
+ class BaseDenseSegmentationModel extends BaseModel {
3113
+ constructor(model_id, providerParams, modelParams) {
3114
+ super(model_id, providerParams, modelParams);
3115
+ __publicField(this, "model");
3116
+ __publicField(this, "processor");
3117
+ /** Tile edge length in pixels. */
3118
+ __publicField(this, "tileSize", 1024);
3119
+ /** Overlap between adjacent tiles for feather blending. */
3120
+ __publicField(this, "tileOverlap", 64);
3121
+ /** Default probability threshold for binarization. */
3122
+ __publicField(this, "defaultThreshold", 0.5);
3123
+ }
3124
+ async initializeModel() {
3125
+ if (this.model) return;
3126
+ this.processor = await ImageProcessor.from_pretrained(this.model_id);
3127
+ const pretrainedModel = await PreTrainedModel.from_pretrained(
3128
+ this.model_id,
3129
+ this.modelParams
3130
+ );
3131
+ this.model = pretrainedModel.sessions.model;
3132
+ }
3133
+ resolveProbabilityOutput(outputs) {
3134
+ const tensor = outputs[this.outputName];
3135
+ if (!tensor) {
3136
+ throw new Error(
3137
+ `Model output "${this.outputName}" not found; got [${Object.keys(outputs).join(", ")}]`
3138
+ );
3139
+ }
3140
+ const dims = tensor.dims;
3141
+ const raw = tensor.data ?? tensor.cpuData;
3142
+ return {
3143
+ type: tensor.type,
3144
+ dims,
3145
+ size: tensor.size,
3146
+ data: raw
3147
+ };
3148
+ }
3149
+ featherWeight(size, overlap) {
3150
+ const ramp = new Float32Array(size);
3151
+ ramp.fill(1);
3152
+ if (overlap > 0) {
3153
+ for (let i = 0; i < overlap; i++) {
3154
+ const t = i / (overlap - 1 || 1);
3155
+ ramp[i] = t;
3156
+ ramp[size - 1 - i] = t;
3157
+ }
3158
+ }
3159
+ const weights = new Float32Array(size * size);
3160
+ for (let y = 0; y < size; y++) {
3161
+ for (let x = 0; x < size; x++) {
3162
+ weights[y * size + x] = ramp[y] * ramp[x];
3163
+ }
3164
+ }
3165
+ return weights;
3166
+ }
3167
+ async runTile(tileImage) {
3168
+ if (!this.processor || !this.model) {
3169
+ throw new Error("Model or processor not initialized");
3170
+ }
3171
+ const inputs = await this.processor(tileImage);
3172
+ const pixelValues = inputs.pixel_values;
3173
+ const dataCopy = pixelValues.data instanceof Float32Array ? new Float32Array(pixelValues.data) : Float32Array.from(pixelValues.data);
3174
+ const feed = new Tensor("float32", dataCopy, pixelValues.dims);
3175
+ let outputs;
3176
+ try {
3177
+ outputs = await this.model.run({
3178
+ [this.inputName]: feed
3179
+ });
3180
+ } catch (err) {
3181
+ 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);
3182
+ throw new Error(`Dense segmentation tile inference failed: ${detail}`);
3183
+ }
3184
+ const { data } = this.resolveProbabilityOutput(outputs);
3185
+ return {
3186
+ data,
3187
+ height: this.tileSize,
3188
+ width: this.tileSize
3189
+ };
3190
+ }
3191
+ /**
3192
+ * Overlapped tiled inference with feather blending, matching the
3193
+ * ChangeStar pipeline (tile_size / overlap / threshold).
3194
+ */
3195
+ async inferDenseMask(geoRawImage, threshold) {
3196
+ const { height, width } = geoRawImage;
3197
+ const tileSize = this.tileSize;
3198
+ const overlap = Math.min(this.tileOverlap, tileSize - 1);
3199
+ const stride = tileSize - overlap;
3200
+ const feather = this.featherWeight(tileSize, overlap);
3201
+ const accum = new Float32Array(height * width);
3202
+ const weightSum = new Float32Array(height * width);
3203
+ for (let rowOff = 0; rowOff < height; rowOff += stride) {
3204
+ for (let colOff = 0; colOff < width; colOff += stride) {
3205
+ const winH = Math.min(tileSize, height - rowOff);
3206
+ const winW = Math.min(tileSize, width - colOff);
3207
+ const croppedRaw = await geoRawImage.crop([
3208
+ colOff,
3209
+ rowOff,
3210
+ colOff + winW - 1,
3211
+ rowOff + winH - 1
3212
+ ]);
3213
+ const padH = tileSize - winH;
3214
+ const padW = tileSize - winW;
3215
+ const paddedRaw = padH > 0 || padW > 0 ? await croppedRaw.pad([0, padW, 0, padH]) : croppedRaw;
3216
+ const tileImage = GeoRawImage.fromRawImage(
3217
+ paddedRaw,
3218
+ geoRawImage.getBounds(),
3219
+ geoRawImage.getCRS()
3220
+ );
3221
+ const { data: prob } = await this.runTile(tileImage);
3222
+ for (let y = 0; y < winH; y++) {
3223
+ for (let x = 0; x < winW; x++) {
3224
+ const srcIdx = y * tileSize + x;
3225
+ const dstIdx = (rowOff + y) * width + (colOff + x);
3226
+ const w = feather[y * tileSize + x];
3227
+ accum[dstIdx] += prob[srcIdx] * w;
3228
+ weightSum[dstIdx] += w;
3229
+ }
3230
+ }
3231
+ }
3232
+ }
3233
+ const binary = new Uint8Array(height * width);
3234
+ for (let i = 0; i < height * width; i++) {
3235
+ const w = weightSum[i] === 0 ? 1 : weightSum[i];
3236
+ binary[i] = accum[i] / w > threshold ? 255 : 0;
3237
+ }
3238
+ const scale = height > 512 || width > 512 ? 2 : 1;
3239
+ const contourH = Math.max(1, Math.floor(height / scale));
3240
+ const contourW = Math.max(1, Math.floor(width / scale));
3241
+ let contourBinary = binary;
3242
+ let contourImage = geoRawImage;
3243
+ if (scale > 1) {
3244
+ contourBinary = new Uint8Array(contourH * contourW);
3245
+ for (let y = 0; y < contourH; y++) {
3246
+ for (let x = 0; x < contourW; x++) {
3247
+ contourBinary[y * contourW + x] = binary[y * scale * width + x * scale];
3248
+ }
3249
+ }
3250
+ contourImage = new GeoRawImage(
3251
+ contourBinary,
3252
+ contourW,
3253
+ contourH,
3254
+ 1,
3255
+ geoRawImage.getBounds(),
3256
+ geoRawImage.getCRS()
3257
+ );
3258
+ }
3259
+ const maskTensor = new Tensor("uint8", contourBinary, [
3260
+ 1,
3261
+ 1,
3262
+ contourH,
3263
+ contourW
3264
+ ]);
3265
+ const masksToFC = maskToGeoJSON({ mask: [maskTensor] }, contourImage);
3266
+ const features = [];
3267
+ masksToFC.forEach((fc) => {
3268
+ fc.features.forEach((feature2) => {
3269
+ feature2.properties = {
3270
+ ...feature2.properties || {},
3271
+ class: "building"
3272
+ };
3273
+ features.push(feature2);
3274
+ });
3275
+ });
3276
+ return {
3277
+ type: "FeatureCollection",
3278
+ features
3279
+ };
3280
+ }
3281
+ async inference(params) {
3282
+ const {
3283
+ inputs: { polygon: polygon2 },
3284
+ postProcessingParams: { confidenceThreshold } = {},
3285
+ mapSourceParams
3286
+ } = params;
3287
+ if (!polygon2) {
3288
+ throw new Error("Polygon input is required for dense segmentation");
3289
+ }
3290
+ if (!polygon2.geometry || polygon2.geometry.type !== "Polygon") {
3291
+ throw new Error("Input must be a valid GeoJSON Polygon feature");
3292
+ }
3293
+ if (!this.initialized) {
3294
+ await this.initialize();
3295
+ }
3296
+ if (!this.model || !this.processor || !this.dataProvider) {
3297
+ throw new Error("Model or data provider not initialized");
3298
+ }
3299
+ const geoRawImage = await this.polygonToImage(
3300
+ polygon2,
3301
+ mapSourceParams == null ? void 0 : mapSourceParams.zoomLevel,
3302
+ mapSourceParams == null ? void 0 : mapSourceParams.bands,
3303
+ mapSourceParams == null ? void 0 : mapSourceParams.expression
3304
+ );
3305
+ const threshold = typeof confidenceThreshold === "number" ? confidenceThreshold : this.defaultThreshold;
3306
+ const taskName = this.model_id.split("/").pop() ?? "dense-segmentation";
3307
+ const inferenceStartTime = performance.now();
3308
+ console.log(`[${taskName}] starting dense segmentation inference...`);
3309
+ const detections = await this.inferDenseMask(geoRawImage, threshold);
3310
+ const inferenceEndTime = performance.now();
3311
+ console.log(
3312
+ `[${taskName}] inference completed. Time taken: ${(inferenceEndTime - inferenceStartTime).toFixed(2)}ms`
3313
+ );
3314
+ return {
3315
+ detections,
3316
+ geoRawImage
3317
+ };
3318
+ }
3319
+ }
3320
+ const _ChangeStarBuildingSegmentation = class _ChangeStarBuildingSegmentation extends BaseDenseSegmentationModel {
3321
+ constructor(model_id, providerParams, modelParams) {
3322
+ super(model_id, providerParams, modelParams);
3323
+ __publicField(this, "inputName", "image");
3324
+ __publicField(this, "outputName", "building_prob");
3325
+ __publicField(this, "tileSize", 1024);
3326
+ __publicField(this, "tileOverlap", 64);
3327
+ __publicField(this, "defaultThreshold", 0.5);
3328
+ }
3329
+ static async getInstance(model_id, providerParams, modelParams) {
3330
+ if (!_ChangeStarBuildingSegmentation.instanceRef || parametersChanged(
3331
+ _ChangeStarBuildingSegmentation.instanceRef,
3332
+ model_id,
3333
+ providerParams,
3334
+ modelParams
3335
+ )) {
3336
+ _ChangeStarBuildingSegmentation.instanceRef = new _ChangeStarBuildingSegmentation(
3337
+ model_id,
3338
+ providerParams,
3339
+ modelParams
3340
+ );
3341
+ await _ChangeStarBuildingSegmentation.instanceRef.initialize();
3342
+ }
3343
+ return { instance: _ChangeStarBuildingSegmentation.instanceRef };
3344
+ }
3345
+ };
3346
+ __publicField(_ChangeStarBuildingSegmentation, "instanceRef", null);
3347
+ __publicField(_ChangeStarBuildingSegmentation, "default_huggingface_id", "geobase/changestar-building-segmentation-vitb");
3348
+ let ChangeStarBuildingSegmentation = _ChangeStarBuildingSegmentation;
3349
+ const _ImageFeatureExtraction = class _ImageFeatureExtraction extends BaseModel {
3350
+ constructor(model_id, providerParams, modelParams) {
3351
+ super(model_id, providerParams, modelParams);
3352
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3353
+ __publicField(this, "extractor");
3354
+ __publicField(this, "patchSize");
3355
+ }
3356
+ static async getInstance(model_id, providerParams, modelParams) {
3357
+ if (!_ImageFeatureExtraction.instance || parametersChanged(
3358
+ _ImageFeatureExtraction.instance,
3359
+ model_id,
3360
+ providerParams,
3361
+ modelParams
3362
+ )) {
3363
+ _ImageFeatureExtraction.instance = new _ImageFeatureExtraction(
3364
+ model_id,
3365
+ providerParams,
3366
+ modelParams
3367
+ );
3368
+ await _ImageFeatureExtraction.instance.initialize();
3369
+ }
3370
+ return { instance: _ImageFeatureExtraction.instance };
3371
+ }
3372
+ async initializeModel() {
3373
+ this.extractor = await pipeline(
3374
+ "image-feature-extraction",
3375
+ this.model_id,
3376
+ this.modelParams
3377
+ );
3378
+ this.extractor.processor.image_processor.do_resize = false;
3379
+ this.patchSize = this.extractor.model.config.patch_size;
3380
+ }
3381
+ /**
3382
+ * Performs image feature extraction on a geographic area specified by a GeoJSON polygon.
3383
+ * Extracts dense feature representations using DINOv3 and computes patch-to-patch similarities.
3384
+ *
3385
+ * @param params - Inference parameters containing polygon and processing options
3386
+ * @returns Promise<ImageFeatureExtractionResults> containing feature vectors, similarity matrix, and metadata
3387
+ * @throws {Error} If data provider, model or processor are not properly initialized
3388
+ */
3389
+ async inference(params) {
3390
+ var _a, _b;
3391
+ const {
3392
+ inputs: { polygon: polygon2 },
3393
+ postProcessingParams: { similarityThreshold = 0.5 } = {},
3394
+ mapSourceParams
3395
+ } = params;
3396
+ if (!polygon2) {
3397
+ throw new Error("Polygon input is required for feature extraction");
3398
+ }
3399
+ if (!polygon2.geometry || polygon2.geometry.type !== "Polygon") {
3400
+ throw new Error("Input must be a valid GeoJSON Polygon feature");
3401
+ }
3402
+ if (!this.initialized) {
3403
+ await this.initialize();
3404
+ }
3405
+ if (!this.extractor || !this.patchSize) {
3406
+ throw new Error("Feature extractor not properly initialized");
3407
+ }
3408
+ const geoRawImage = await this.polygonToImage(
3409
+ polygon2,
3410
+ mapSourceParams == null ? void 0 : mapSourceParams.zoomLevel,
3411
+ mapSourceParams == null ? void 0 : mapSourceParams.bands,
3412
+ mapSourceParams == null ? void 0 : mapSourceParams.expression
3413
+ );
3414
+ const inferenceStartTime = performance.now();
3415
+ console.log("[image-feature-extraction] starting inference...");
3416
+ try {
3417
+ const features = await this.extractor(geoRawImage, {
3418
+ pooling: "none"
3419
+ });
3420
+ const numRegisterTokens = this.extractor.model.config.num_register_tokens ?? 0;
3421
+ const startIndex = 1 + numRegisterTokens;
3422
+ const patchFeatures = features.slice(null, [startIndex, null]);
3423
+ const normalizedFeatures = patchFeatures.normalize(2, -1);
3424
+ const scores = await matmul(
3425
+ normalizedFeatures,
3426
+ normalizedFeatures.permute(0, 2, 1)
3427
+ );
3428
+ const similarityMatrix = (await scores.tolist())[0];
3429
+ const featureVectors = (await patchFeatures.tolist())[0];
3430
+ let filteredFeatures = featureVectors;
3431
+ let filteredSimilarityMatrix = similarityMatrix;
3432
+ if (similarityThreshold > 0) {
3433
+ const highSimilarityPatches = /* @__PURE__ */ new Set();
3434
+ for (let i = 0; i < similarityMatrix.length; i++) {
3435
+ for (let j = 0; j < similarityMatrix[i].length; j++) {
3436
+ if (i !== j && similarityMatrix[i][j] >= similarityThreshold) {
3437
+ highSimilarityPatches.add(i);
3438
+ highSimilarityPatches.add(j);
3439
+ }
3440
+ }
3441
+ }
3442
+ const patchIndices = Array.from(highSimilarityPatches).sort(
3443
+ (a, b) => a - b
3444
+ );
3445
+ filteredFeatures = patchIndices.map((i) => featureVectors[i]);
3446
+ filteredSimilarityMatrix = patchIndices.map(
3447
+ (i) => patchIndices.map((j) => similarityMatrix[i][j])
3448
+ );
3449
+ }
3450
+ const inferenceEndTime = performance.now();
3451
+ console.log(
3452
+ `[image-feature-extraction] inference completed. Time taken: ${(inferenceEndTime - inferenceStartTime).toFixed(2)}ms`
3453
+ );
3454
+ const result = {
3455
+ features: filteredFeatures,
3456
+ similarityMatrix: filteredSimilarityMatrix,
3457
+ patchSize: this.patchSize,
3458
+ geoRawImage,
3459
+ metadata: {
3460
+ numPatches: filteredFeatures.length,
3461
+ featureDimensions: ((_a = filteredFeatures[0]) == null ? void 0 : _a.length) || 0,
3462
+ modelId: this.model_id
3463
+ }
3464
+ };
3465
+ console.log("[image-feature-extraction] Result structure:", {
3466
+ featuresLength: result.features.length,
3467
+ similarityMatrixLength: result.similarityMatrix.length,
3468
+ patchSize: result.patchSize,
3469
+ geoRawImageBounds: (_b = result.geoRawImage) == null ? void 0 : _b.getBounds(),
3470
+ metadata: result.metadata
3471
+ });
3472
+ return result;
3473
+ } catch (error) {
3474
+ console.error("Feature extraction error:", error);
3475
+ throw new Error(
3476
+ `Feature extraction failed: ${error instanceof Error ? error.message : "Unknown error"}`
3477
+ );
3478
+ }
3479
+ }
3480
+ };
3481
+ __publicField(_ImageFeatureExtraction, "instance", null);
3482
+ let ImageFeatureExtraction = _ImageFeatureExtraction;
3000
3483
  const _BuildingFootPrintSegmentation = class _BuildingFootPrintSegmentation extends BaseModel {
3001
3484
  constructor(model_id, providerParams, modelParams) {
3002
3485
  super(model_id, providerParams, modelParams);
@@ -3166,141 +3649,42 @@ const _BuildingFootPrintSegmentation = class _BuildingFootPrintSegmentation exte
3166
3649
  }
3167
3650
  };
3168
3651
  __publicField(_BuildingFootPrintSegmentation, "instance", null);
3652
+ __publicField(_BuildingFootPrintSegmentation, "default_huggingface_id", "geobase/building-footprint-segmentation");
3169
3653
  let BuildingFootPrintSegmentation = _BuildingFootPrintSegmentation;
3170
- const _ImageFeatureExtraction = class _ImageFeatureExtraction extends BaseModel {
3171
- constructor(model_id, providerParams, modelParams) {
3172
- super(model_id, providerParams, modelParams);
3173
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3174
- __publicField(this, "extractor");
3175
- __publicField(this, "patchSize");
3176
- }
3177
- static async getInstance(model_id, providerParams, modelParams) {
3178
- if (!_ImageFeatureExtraction.instance || parametersChanged(
3179
- _ImageFeatureExtraction.instance,
3180
- model_id,
3181
- providerParams,
3182
- modelParams
3183
- )) {
3184
- _ImageFeatureExtraction.instance = new _ImageFeatureExtraction(
3185
- model_id,
3186
- providerParams,
3187
- modelParams
3654
+ const buildingFootprintFactory = (params, modelId, modelParams) => BuildingFootPrintSegmentation.getInstance(modelId, params, modelParams);
3655
+ const changeStarFactory = (params, modelId, modelParams) => ChangeStarBuildingSegmentation.getInstance(
3656
+ modelId,
3657
+ params,
3658
+ modelParams ?? { dtype: "fp32" }
3659
+ );
3660
+ const buildingSegmentationFactories = {
3661
+ [BuildingFootPrintSegmentation.default_huggingface_id]: buildingFootprintFactory,
3662
+ [ChangeStarBuildingSegmentation.default_huggingface_id]: changeStarFactory
3663
+ };
3664
+ const BuildingSegmentationFactory = {
3665
+ get knownModelIds() {
3666
+ return Object.keys(buildingSegmentationFactories);
3667
+ },
3668
+ getInstance(params, modelId, modelParams) {
3669
+ const resolvedId = modelId == null ? void 0 : modelId.trim();
3670
+ if (!resolvedId) {
3671
+ return Promise.reject(
3672
+ new Error(
3673
+ `Building segmentation modelId is required. Known ids: ${this.knownModelIds.join(", ")}`
3674
+ )
3188
3675
  );
3189
- await _ImageFeatureExtraction.instance.initialize();
3190
3676
  }
3191
- return { instance: _ImageFeatureExtraction.instance };
3192
- }
3193
- async initializeModel() {
3194
- this.extractor = await pipeline(
3195
- "image-feature-extraction",
3196
- this.model_id,
3197
- this.modelParams
3198
- );
3199
- this.extractor.processor.image_processor.do_resize = false;
3200
- this.patchSize = this.extractor.model.config.patch_size;
3201
- }
3202
- /**
3203
- * Performs image feature extraction on a geographic area specified by a GeoJSON polygon.
3204
- * Extracts dense feature representations using DINOv3 and computes patch-to-patch similarities.
3205
- *
3206
- * @param params - Inference parameters containing polygon and processing options
3207
- * @returns Promise<ImageFeatureExtractionResults> containing feature vectors, similarity matrix, and metadata
3208
- * @throws {Error} If data provider, model or processor are not properly initialized
3209
- */
3210
- async inference(params) {
3211
- var _a, _b;
3212
- const {
3213
- inputs: { polygon: polygon2 },
3214
- postProcessingParams: { similarityThreshold = 0.5 } = {},
3215
- mapSourceParams
3216
- } = params;
3217
- if (!polygon2) {
3218
- throw new Error("Polygon input is required for feature extraction");
3219
- }
3220
- if (!polygon2.geometry || polygon2.geometry.type !== "Polygon") {
3221
- throw new Error("Input must be a valid GeoJSON Polygon feature");
3222
- }
3223
- if (!this.initialized) {
3224
- await this.initialize();
3225
- }
3226
- if (!this.extractor || !this.patchSize) {
3227
- throw new Error("Feature extractor not properly initialized");
3228
- }
3229
- const geoRawImage = await this.polygonToImage(
3230
- polygon2,
3231
- mapSourceParams == null ? void 0 : mapSourceParams.zoomLevel,
3232
- mapSourceParams == null ? void 0 : mapSourceParams.bands,
3233
- mapSourceParams == null ? void 0 : mapSourceParams.expression
3234
- );
3235
- const inferenceStartTime = performance.now();
3236
- console.log("[image-feature-extraction] starting inference...");
3237
- try {
3238
- const features = await this.extractor(geoRawImage, {
3239
- pooling: "none"
3240
- });
3241
- const numRegisterTokens = this.extractor.model.config.num_register_tokens ?? 0;
3242
- const startIndex = 1 + numRegisterTokens;
3243
- const patchFeatures = features.slice(null, [startIndex, null]);
3244
- const normalizedFeatures = patchFeatures.normalize(2, -1);
3245
- const scores = await matmul(
3246
- normalizedFeatures,
3247
- normalizedFeatures.permute(0, 2, 1)
3248
- );
3249
- const similarityMatrix = (await scores.tolist())[0];
3250
- const featureVectors = (await patchFeatures.tolist())[0];
3251
- let filteredFeatures = featureVectors;
3252
- let filteredSimilarityMatrix = similarityMatrix;
3253
- if (similarityThreshold > 0) {
3254
- const highSimilarityPatches = /* @__PURE__ */ new Set();
3255
- for (let i = 0; i < similarityMatrix.length; i++) {
3256
- for (let j = 0; j < similarityMatrix[i].length; j++) {
3257
- if (i !== j && similarityMatrix[i][j] >= similarityThreshold) {
3258
- highSimilarityPatches.add(i);
3259
- highSimilarityPatches.add(j);
3260
- }
3261
- }
3262
- }
3263
- const patchIndices = Array.from(highSimilarityPatches).sort(
3264
- (a, b) => a - b
3265
- );
3266
- filteredFeatures = patchIndices.map((i) => featureVectors[i]);
3267
- filteredSimilarityMatrix = patchIndices.map(
3268
- (i) => patchIndices.map((j) => similarityMatrix[i][j])
3269
- );
3270
- }
3271
- const inferenceEndTime = performance.now();
3272
- console.log(
3273
- `[image-feature-extraction] inference completed. Time taken: ${(inferenceEndTime - inferenceStartTime).toFixed(2)}ms`
3274
- );
3275
- const result = {
3276
- features: filteredFeatures,
3277
- similarityMatrix: filteredSimilarityMatrix,
3278
- patchSize: this.patchSize,
3279
- geoRawImage,
3280
- metadata: {
3281
- numPatches: filteredFeatures.length,
3282
- featureDimensions: ((_a = filteredFeatures[0]) == null ? void 0 : _a.length) || 0,
3283
- modelId: this.model_id
3284
- }
3285
- };
3286
- console.log("[image-feature-extraction] Result structure:", {
3287
- featuresLength: result.features.length,
3288
- similarityMatrixLength: result.similarityMatrix.length,
3289
- patchSize: result.patchSize,
3290
- geoRawImageBounds: (_b = result.geoRawImage) == null ? void 0 : _b.getBounds(),
3291
- metadata: result.metadata
3292
- });
3293
- return result;
3294
- } catch (error) {
3295
- console.error("Feature extraction error:", error);
3296
- throw new Error(
3297
- `Feature extraction failed: ${error instanceof Error ? error.message : "Unknown error"}`
3677
+ const create = buildingSegmentationFactories[resolvedId];
3678
+ if (!create) {
3679
+ return Promise.reject(
3680
+ new Error(
3681
+ `Unknown building segmentation modelId "${resolvedId}". Known ids: ${this.knownModelIds.join(", ")}`
3682
+ )
3298
3683
  );
3299
3684
  }
3685
+ return create(params, resolvedId, modelParams);
3300
3686
  }
3301
3687
  };
3302
- __publicField(_ImageFeatureExtraction, "instance", null);
3303
- let ImageFeatureExtraction = _ImageFeatureExtraction;
3304
3688
  const modelRegistry = [
3305
3689
  {
3306
3690
  task: "zero-shot-object-detection",
@@ -3485,20 +3869,14 @@ const modelRegistry = [
3485
3869
  {
3486
3870
  task: "building-footprint-segmentation",
3487
3871
  library: "geoai",
3488
- description: "Segments the precise outlines (footprints) of buildings in imagery. Useful for mapping, urban planning, or disaster assessment.",
3872
+ 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.",
3489
3873
  examples: [
3490
3874
  "Segment building footprints in this city block.",
3491
3875
  "Identify the outlines of all buildings in this image.",
3492
3876
  "Find building perimeters in this urban area."
3493
3877
  ],
3494
3878
  ioConfig: {},
3495
- geobase_ai_pipeline: (params, modelId = "geobase/building-footprint-segmentation", modelParams) => {
3496
- return BuildingFootPrintSegmentation.getInstance(
3497
- modelId,
3498
- params,
3499
- modelParams
3500
- );
3501
- }
3879
+ geobase_ai_pipeline: (params, modelId = ChangeStarBuildingSegmentation.default_huggingface_id, modelParams) => BuildingSegmentationFactory.getInstance(params, modelId, modelParams)
3502
3880
  },
3503
3881
  {
3504
3882
  task: "image-feature-extraction",
package/index.d.ts CHANGED
@@ -162,10 +162,46 @@ declare class Tms extends MapSource {
162
162
  number
163
163
  ], instance: Tms): string;
164
164
  }
165
+ export type WmsVersion = "1.1.1" | "1.3.0";
166
+ export type WmsCrs = "EPSG:3857" | "EPSG:4326";
167
+ export interface WmsGetMapConfig {
168
+ baseUrl: string;
169
+ layers: string;
170
+ version?: WmsVersion;
171
+ crs?: WmsCrs;
172
+ format?: string;
173
+ styles?: string;
174
+ transparent?: boolean;
175
+ tileSize?: number;
176
+ attribution?: string;
177
+ headers?: Record<string, string>;
178
+ extraParams?: Record<string, string>;
179
+ }
180
+ export interface WmsConfig extends WmsGetMapConfig {
181
+ }
182
+ declare class Wms extends MapSource {
183
+ baseUrl: string;
184
+ layers: string;
185
+ version: WmsVersion;
186
+ crs: WmsCrs;
187
+ format: string;
188
+ styles: string;
189
+ transparent: boolean;
190
+ attribution: string;
191
+ tileSize: number;
192
+ headers?: Record<string, string>;
193
+ extraParams?: Record<string, string>;
194
+ constructor(config: WmsConfig);
195
+ protected getTileUrlFromTileCoords(tileCoords: [
196
+ number,
197
+ number,
198
+ number
199
+ ], instance: Wms): string;
200
+ }
165
201
  declare abstract class BaseModel {
166
202
  protected static instance: BaseModel | null;
167
203
  protected providerParams: ProviderParams;
168
- protected dataProvider: Mapbox | Geobase | Esri | Tms | undefined;
204
+ protected dataProvider: Mapbox | Geobase | Esri | Tms | Wms | undefined;
169
205
  protected model_id: string;
170
206
  protected initialized: boolean;
171
207
  protected modelParams?: PretrainedModelOptions;
@@ -411,6 +447,7 @@ declare class BuildingFootPrintSegmentation extends BaseModel {
411
447
  protected static instance: BuildingFootPrintSegmentation | null;
412
448
  protected model: ort.InferenceSession | undefined;
413
449
  protected processor: ImageProcessor | undefined;
450
+ static readonly default_huggingface_id = "geobase/building-footprint-segmentation";
414
451
  private constructor();
415
452
  static getInstance(model_id: string, providerParams: ProviderParams, modelParams?: PretrainedModelOptions): Promise<{
416
453
  instance: BuildingFootPrintSegmentation;
@@ -438,6 +475,53 @@ declare class ImageFeatureExtraction extends BaseModel {
438
475
  */
439
476
  inference(params: InferenceParams): Promise<ImageFeatureExtractionResults>;
440
477
  }
478
+ declare abstract class BaseDenseSegmentationModel extends BaseModel {
479
+ protected model: ort.InferenceSession | undefined;
480
+ protected processor: ImageProcessor | undefined;
481
+ /** ONNX input feed name (e.g. "image"). */
482
+ protected abstract readonly inputName: string;
483
+ /** Soft-probability output tensor name (e.g. "building_prob"). */
484
+ protected abstract readonly outputName: string;
485
+ /** Tile edge length in pixels. */
486
+ protected readonly tileSize: number;
487
+ /** Overlap between adjacent tiles for feather blending. */
488
+ protected readonly tileOverlap: number;
489
+ /** Default probability threshold for binarization. */
490
+ protected readonly defaultThreshold: number;
491
+ protected constructor(model_id: string, providerParams: ProviderParams, modelParams?: PretrainedModelOptions);
492
+ protected initializeModel(): Promise<void>;
493
+ protected resolveProbabilityOutput(outputs: Record<string, any>): {
494
+ type: string;
495
+ dims: number[];
496
+ size: number;
497
+ data: Float32Array;
498
+ };
499
+ protected featherWeight(size: number, overlap: number): Float32Array;
500
+ protected runTile(tileImage: GeoRawImage): Promise<{
501
+ data: Float32Array;
502
+ height: number;
503
+ width: number;
504
+ }>;
505
+ /**
506
+ * Overlapped tiled inference with feather blending, matching the
507
+ * ChangeStar pipeline (tile_size / overlap / threshold).
508
+ */
509
+ protected inferDenseMask(geoRawImage: GeoRawImage, threshold: number): Promise<GeoJSON.FeatureCollection>;
510
+ inference(params: InferenceParams): Promise<ObjectDetectionResults>;
511
+ }
512
+ declare class ChangeStarBuildingSegmentation extends BaseDenseSegmentationModel {
513
+ protected static instanceRef: ChangeStarBuildingSegmentation | null;
514
+ static readonly default_huggingface_id = "geobase/changestar-building-segmentation-vitb";
515
+ protected readonly inputName = "image";
516
+ protected readonly outputName = "building_prob";
517
+ protected readonly tileSize = 1024;
518
+ protected readonly tileOverlap = 64;
519
+ protected readonly defaultThreshold = 0.5;
520
+ private constructor();
521
+ static getInstance(model_id: string, providerParams: ProviderParams, modelParams?: PretrainedModelOptions): Promise<{
522
+ instance: ChangeStarBuildingSegmentation;
523
+ }>;
524
+ }
441
525
  export type MapboxParams = {
442
526
  provider: "mapbox";
443
527
  apiKey: string;
@@ -470,6 +554,20 @@ export type TmsParams = {
470
554
  headers?: Record<string, string>;
471
555
  scheme?: "WebMercator" | "TMS";
472
556
  };
557
+ export type WmsParams = {
558
+ provider: "wms";
559
+ baseUrl: string;
560
+ layers: string;
561
+ version?: "1.1.1" | "1.3.0";
562
+ crs?: "EPSG:3857" | "EPSG:4326";
563
+ format?: string;
564
+ styles?: string;
565
+ transparent?: boolean;
566
+ attribution?: string;
567
+ tileSize?: number;
568
+ headers?: Record<string, string>;
569
+ extraParams?: Record<string, string>;
570
+ };
473
571
  export interface InferenceInputs {
474
572
  polygon: GeoJSON.Feature;
475
573
  classLabel?: string;
@@ -488,10 +586,10 @@ export interface InferenceParams {
488
586
  postProcessingParams?: PostProcessingParams;
489
587
  mapSourceParams?: MapSourceParams;
490
588
  }
491
- export type ProviderParams = MapboxParams | SentinelParams | GeobaseParams | EsriParams | TmsParams;
589
+ export type ProviderParams = MapboxParams | SentinelParams | GeobaseParams | EsriParams | TmsParams | WmsParams;
492
590
  export type HuggingFaceModelTask = "mask-generation" | "zero-shot-object-detection" | "zero-shot-image-classification" | "object-detection" | "oriented-object-detection" | "image-feature-extraction";
493
591
  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";
494
- export type ModelInstance = MaskGeneration | ZeroShotObjectDetection | ObjectDetection | OrientedObjectDetection | LandCoverClassification | SolarPanelDetection | ShipDetection | CarDetection | WetLandSegmentation | BuildingDetection | OilStorageTankDetection | BuildingFootPrintSegmentation | ImageFeatureExtraction;
592
+ export type ModelInstance = MaskGeneration | ZeroShotObjectDetection | ObjectDetection | OrientedObjectDetection | LandCoverClassification | SolarPanelDetection | ShipDetection | CarDetection | WetLandSegmentation | BuildingDetection | ChangeStarBuildingSegmentation | OilStorageTankDetection | BuildingFootPrintSegmentation | ImageFeatureExtraction;
495
593
  export type ModelConfig = {
496
594
  task: HuggingFaceModelTask | GeobaseAiModelTask;
497
595
  library: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geoai",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "A JavaScript library for running Geo AI models in frontend applications",
5
5
  "keywords": [
6
6
  "geoai",