Skip to content

Commit

Permalink
Merge pull request #86 from chekhovana/clustering
Browse files Browse the repository at this point in the history
Add clustering to provide low tile detalization
  • Loading branch information
bertt authored Nov 25, 2024
2 parents 82a6b36 + cccb8f3 commit 765fb46
Show file tree
Hide file tree
Showing 7 changed files with 2,659 additions and 10 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Tool parameters:
--use_external_model: (optional - default false) Use external model instead of embedded model (Only when creating I3dm's)
--use_clustering: (optional - default false) If tile contains more than max_features_per_tile instances, its number of instances will be reduced to max_features_per_tile by clustering
```

# Docker
Expand Down Expand Up @@ -293,6 +295,24 @@ Known issues GPU Instancing:
- Getting attributes in Cesium does not work when there are multiple input models
https://community.cesium.com/t/upgrade-3d-tileset-with-composite-cmpt-tile-to-1-1-attribute-data-missing/33177/2

## Clustering

There is an experimental option to create 3D Tiles using clustering: --use_clustering (default false).

When this option is off, dense tiles with number of instances exceeding `max_features_per_tile` aren't rendered. With this option such tiles are rendered with number of instances that is exactly equal to `max_features_per_tile`. Number of instances is reduced in the following way:

- tile instances are clustered with MiniBatchKMeans algorithm with number of clusters equal to `max_features_per_tile`;
- from each cluster single instance is picked randomly.

### Performance benchmark
number of instances: 2500<br>
max_features_per_tile: 100<br>

tileset generation time:
- without clustering : 0h 0m 0s 539ms
- with clustering: 0h 0m 1s 238ms


## Developing

Run from source code:
Expand Down
46 changes: 36 additions & 10 deletions src/ImplicitTiling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,23 @@ public static List<Tile> GenerateTiles(Options o, NpgsqlConnection conn, Boundin
}
else if (numberOfFeatures > o.MaxFeaturesPerTile)
{
tile.Available = false;
if ((bool)o.UseClustering)
{
tile.Available = true;
string tileName = $"{tile.Z}_{tile.X}_{tile.Y}";
string message = $"Getting {numberOfFeatures} instances to create tile {tileName}";
Console.Write($"\r{new string(' ', Console.WindowWidth - 1)}\r{message}");
var instances = InstancesRepository.GetInstances(conn, o.Table, o.GeometryColumn, bbox, source_epsg, where, (bool)o.UseScaleNonUniform, useGpuInstancing);
message = $"Clustering tile {tileName} with {numberOfFeatures} instances";
Console.Write($"\r{new string(' ', Console.WindowWidth - 1)}\r{message}");
instances = TileClustering.Cluster(instances, o.MaxFeaturesPerTile);
var bytes = CreateTile(o, instances, useGpuInstancing, useI3dm);
SaveTile(contentDirectory, tile, bytes, useGpuInstancing, useI3dm);
}
else
{
tile.Available = false;
}
tiles.Add(tile);

// split in quadtree
Expand All @@ -71,15 +87,7 @@ public static List<Tile> GenerateTiles(Options o, NpgsqlConnection conn, Boundin
else
{
var bytes = CreateTile(o, conn, bbox, source_epsg, where, useGpuInstancing, useI3dm);
var extension = useGpuInstancing? "glb": "cmpt";
if (useI3dm)
{
extension = "i3dm";
}
var file = $"{contentDirectory}{Path.AltDirectorySeparatorChar}{tile.Z}_{tile.X}_{tile.Y}.{extension}";
Console.Write($"\rCreating tile: {file} ");

File.WriteAllBytes(file, bytes);
SaveTile(contentDirectory, tile, bytes, useGpuInstancing, useI3dm);

var t1 = new Tile(tile.Z, tile.X, tile.Y);
t1.Available = true;
Expand All @@ -89,9 +97,27 @@ public static List<Tile> GenerateTiles(Options o, NpgsqlConnection conn, Boundin
return tiles;
}

private static void SaveTile(string contentDirectory, Tile tile, byte[] bytes, bool useGpuInstancing, bool useI3dm)
{
var extension = useGpuInstancing? "glb": "cmpt";
if (useI3dm)
{
extension = "i3dm";
}
var file = $"{contentDirectory}{Path.AltDirectorySeparatorChar}{tile.Z}_{tile.X}_{tile.Y}.{extension}";
Console.Write($"\rCreating tile: {file} ");

File.WriteAllBytes(file, bytes);
}

private static byte[] CreateTile(Options o, NpgsqlConnection conn, BoundingBox tileBounds, int source_epsg, string where, bool useGpuInstancing = false, bool useI3dm = false)
{
var instances = InstancesRepository.GetInstances(conn, o.Table, o.GeometryColumn, tileBounds, source_epsg, where, (bool)o.UseScaleNonUniform, useGpuInstancing);
return CreateTile(o, instances, useGpuInstancing, useI3dm);
}

private static byte[] CreateTile(Options o, List<Instance> instances, bool useGpuInstancing, bool useI3dm)
{
byte[] tile;

if (useGpuInstancing)
Expand Down
4 changes: 4 additions & 0 deletions src/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public class Options

[Option("use_external_model", Required = false, Default = false, HelpText = "Use external model")]
public bool? UseExternalModel { get; set; }

[Option("use_clustering", Required = false, Default = false, HelpText = "Use clustering")]
public bool? UseClustering { get; set; }

}


Expand Down
45 changes: 45 additions & 0 deletions src/TileClustering.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using Accord.MachineLearning;
using Wkx;

namespace i3dm.export;

public static class TileClustering
{
public static List<Instance> Cluster(List<Instance> instances, int size)
{
var data = instances.Select(instance => instance.Position)
.OfType<Point>()
.Select(pt => new double[] { (double)pt.X, (double)pt.Y, (double)pt.Z })
.ToList();
double[][] matrix = data.ToArray();
KMeans kmeans = new MiniBatchKMeans(k: size, batchSize: 10) // this batchSize is optimal in terms of performance
{
MaxIterations = 100,
Tolerance = 1e-3,
// based on https://scikit-learn.org/dev/modules/generated/sklearn.cluster.MiniBatchKMeans.html
// without this parameter Learn method sometimes hangs
InitializationBatchSize = size * 3
};
KMeansClusterCollection clusters = kmeans.Learn(matrix);
int[] labels = clusters.Decide(matrix);
Instance[] result = new Instance[size];
int count = 0;
foreach (var (instance, label) in instances.Zip(labels))
{
if (result[label] == null)
{
result[label] = instance;
count++;
if (count == size)
{
break;
}
}
}
return result.ToList();
}
}
1 change: 1 addition & 0 deletions src/i3dm.export.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Accord.MachineLearning" Version="3.8.0" />
<PackageReference Include="cmpt-tile" Version="0.2.4" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Dapper" Version="2.1.35" />
Expand Down
37 changes: 37 additions & 0 deletions tests/Clustering/ClusteringTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using i3dm.export;

namespace i3dm.export.tests.Clustering;

public class ClusteringTests
{

[Test]
public void TestNumberOfClusters()
{
Assert.That(true);

// create 1000 random instances
var random = new Random();
var instances = new List<Instance>();
for (int i = 0; i < 1000; i++)
{
var x = random.NextDouble() * 1000;
var y = random.NextDouble() * 1000;
instances.Add(new Instance
{
Position = new Wkx.Point(x, y, 0),
Scale = 1,
Rotation = 0
});
}

Assert.That(instances.Count, Is.EqualTo(1000));

// cluster them into 10 groups
var clusters = TileClustering.Cluster(instances, 10);
Assert.That(clusters.Count, Is.EqualTo(10));
}
}
Loading

0 comments on commit 765fb46

Please sign in to comment.