geoai 1.0.5 → 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,16 @@ 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
+
10
20
  ## [1.0.5] - 2026-07-15
11
21
 
12
22
  ### Added
@@ -16,9 +26,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
26
  - WMS documentation (`wms.mdx`), map-providers table update, and 17 WMS tests (including pipeline inference)
17
27
  - Project roadmap (`_docs4devs/ROADMAP.md`) for use-case guides and quantization pipeline backlog
18
28
 
19
- ### Changed
20
- - `live-examples-nextjs` pins `geoai` to local `build/` for WMS/TMS provider development
21
-
22
29
  ## [1.0.4] - 2026-07-11
23
30
 
24
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.5/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.5/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`.
@@ -248,4 +248,4 @@ We welcome contributions! Please see our [contributing guide](CONTRIBUTING.md) f
248
248
 
249
249
  MIT License - see [LICENSE.md](LICENSE.md) for details.
250
250
 
251
- [//]: <> (Toggle CI on)
251
+ [//]: <> (Toggle CI off)
package/geoai.js CHANGED
@@ -3109,6 +3109,377 @@ const _OilStorageTankDetection = class _OilStorageTankDetection extends BaseMode
3109
3109
  };
3110
3110
  __publicField(_OilStorageTankDetection, "instance", null);
3111
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;
3112
3483
  const _BuildingFootPrintSegmentation = class _BuildingFootPrintSegmentation extends BaseModel {
3113
3484
  constructor(model_id, providerParams, modelParams) {
3114
3485
  super(model_id, providerParams, modelParams);
@@ -3278,141 +3649,42 @@ const _BuildingFootPrintSegmentation = class _BuildingFootPrintSegmentation exte
3278
3649
  }
3279
3650
  };
3280
3651
  __publicField(_BuildingFootPrintSegmentation, "instance", null);
3652
+ __publicField(_BuildingFootPrintSegmentation, "default_huggingface_id", "geobase/building-footprint-segmentation");
3281
3653
  let BuildingFootPrintSegmentation = _BuildingFootPrintSegmentation;
3282
- const _ImageFeatureExtraction = class _ImageFeatureExtraction extends BaseModel {
3283
- constructor(model_id, providerParams, modelParams) {
3284
- super(model_id, providerParams, modelParams);
3285
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3286
- __publicField(this, "extractor");
3287
- __publicField(this, "patchSize");
3288
- }
3289
- static async getInstance(model_id, providerParams, modelParams) {
3290
- if (!_ImageFeatureExtraction.instance || parametersChanged(
3291
- _ImageFeatureExtraction.instance,
3292
- model_id,
3293
- providerParams,
3294
- modelParams
3295
- )) {
3296
- _ImageFeatureExtraction.instance = new _ImageFeatureExtraction(
3297
- model_id,
3298
- providerParams,
3299
- 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
+ )
3300
3675
  );
3301
- await _ImageFeatureExtraction.instance.initialize();
3302
3676
  }
3303
- return { instance: _ImageFeatureExtraction.instance };
3304
- }
3305
- async initializeModel() {
3306
- this.extractor = await pipeline(
3307
- "image-feature-extraction",
3308
- this.model_id,
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"}`
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
+ )
3410
3683
  );
3411
3684
  }
3685
+ return create(params, resolvedId, modelParams);
3412
3686
  }
3413
3687
  };
3414
- __publicField(_ImageFeatureExtraction, "instance", null);
3415
- let ImageFeatureExtraction = _ImageFeatureExtraction;
3416
3688
  const modelRegistry = [
3417
3689
  {
3418
3690
  task: "zero-shot-object-detection",
@@ -3597,20 +3869,14 @@ const modelRegistry = [
3597
3869
  {
3598
3870
  task: "building-footprint-segmentation",
3599
3871
  library: "geoai",
3600
- 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.",
3601
3873
  examples: [
3602
3874
  "Segment building footprints in this city block.",
3603
3875
  "Identify the outlines of all buildings in this image.",
3604
3876
  "Find building perimeters in this urban area."
3605
3877
  ],
3606
3878
  ioConfig: {},
3607
- geobase_ai_pipeline: (params, modelId = "geobase/building-footprint-segmentation", modelParams) => {
3608
- return BuildingFootPrintSegmentation.getInstance(
3609
- modelId,
3610
- params,
3611
- modelParams
3612
- );
3613
- }
3879
+ geobase_ai_pipeline: (params, modelId = ChangeStarBuildingSegmentation.default_huggingface_id, modelParams) => BuildingSegmentationFactory.getInstance(params, modelId, modelParams)
3614
3880
  },
3615
3881
  {
3616
3882
  task: "image-feature-extraction",
package/index.d.ts CHANGED
@@ -447,6 +447,7 @@ declare class BuildingFootPrintSegmentation extends BaseModel {
447
447
  protected static instance: BuildingFootPrintSegmentation | null;
448
448
  protected model: ort.InferenceSession | undefined;
449
449
  protected processor: ImageProcessor | undefined;
450
+ static readonly default_huggingface_id = "geobase/building-footprint-segmentation";
450
451
  private constructor();
451
452
  static getInstance(model_id: string, providerParams: ProviderParams, modelParams?: PretrainedModelOptions): Promise<{
452
453
  instance: BuildingFootPrintSegmentation;
@@ -474,6 +475,53 @@ declare class ImageFeatureExtraction extends BaseModel {
474
475
  */
475
476
  inference(params: InferenceParams): Promise<ImageFeatureExtractionResults>;
476
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
+ }
477
525
  export type MapboxParams = {
478
526
  provider: "mapbox";
479
527
  apiKey: string;
@@ -541,7 +589,7 @@ export interface InferenceParams {
541
589
  export type ProviderParams = MapboxParams | SentinelParams | GeobaseParams | EsriParams | TmsParams | WmsParams;
542
590
  export type HuggingFaceModelTask = "mask-generation" | "zero-shot-object-detection" | "zero-shot-image-classification" | "object-detection" | "oriented-object-detection" | "image-feature-extraction";
543
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";
544
- 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;
545
593
  export type ModelConfig = {
546
594
  task: HuggingFaceModelTask | GeobaseAiModelTask;
547
595
  library: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geoai",
3
- "version": "1.0.5",
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",