Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add clustering to provide low tile detalization #86

Merged
merged 4 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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