├── .gitignore ├── LICENSE ├── PointcloudTool ├── HoleFixer.cs ├── PointHashSet.cs ├── PointcloudTool.cs ├── PointcloudTool.csproj ├── Properties │ └── AssemblyInfo.cs ├── STLFile.cs ├── SolidMeshCreator.cs ├── SquareExtractor.cs ├── Triangle.cs ├── Vector3.cs ├── XYZFile.cs └── XYZFileWriter.cs ├── README.md ├── create_mesh.bat ├── filter_script.mlx ├── intersect.py └── pointcloudtool.sln /.gitignore: -------------------------------------------------------------------------------- 1 | # use glob syntax 2 | syntax: glob 3 | 4 | data/** 5 | *.stl 6 | *.xyz 7 | 8 | *.obj 9 | *.pdb 10 | *.user 11 | *.aps 12 | *.pch 13 | *.vspscc 14 | *.vssscc 15 | *_i.c 16 | *_p.c 17 | *.ncb 18 | *.suo 19 | *.tlb 20 | *.tlh 21 | *.bak 22 | *.cache 23 | *.ilk 24 | *.log 25 | *.lib 26 | *.sbr 27 | *.scc 28 | [Bb]in 29 | [Dd]ebug*/ 30 | obj/ 31 | [Rr]elease*/ 32 | _ReSharper*/ 33 | [Tt]humbs.db 34 | [Tt]est[Rr]esult* 35 | [Bb]uild[Ll]og.* 36 | *.[Pp]ublish.xml 37 | *.resharper 38 | [Bb]uild*/ 39 | .metadata\ 40 | gen\ 41 | bin\ 42 | .settings -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Marian Kleineberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PointcloudTool/HoleFixer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | class HoleFixer { 8 | private Vector3[] points; 9 | private PointHashSet hashSet; 10 | 11 | public HoleFixer(Vector3[] points) { 12 | this.points = points; 13 | this.hashSet = new PointHashSet(2.0, points); 14 | } 15 | 16 | public IEnumerable GetEdgePoints() { 17 | const double range = 1.0; 18 | for (int i = 0; i < this.points.Length; i++) { 19 | var point = this.points[i]; 20 | var neighborhood = this.hashSet.GetPointsInRange(point, range, false).Where(p => (p - point).Length < range && (p - point).Length > 0.01).Select(p => p - point).ToArray(); 21 | 22 | var sum = new Vector3(0,0,0); 23 | foreach (var neighbour in neighborhood) { 24 | sum += new Vector3(neighbour.x, 0, neighbour.z).Normalized; 25 | } 26 | sum /= neighborhood.Length; 27 | 28 | 29 | if (neighborhood.Length > 6 && sum.Length > 0.35) { 30 | yield return point; 31 | } 32 | 33 | if (i % 2000 == 0) { 34 | Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0:0.00}% Fixing holes", ((double)i / this.points.Length * 100.0))); 35 | } 36 | } 37 | yield break; 38 | } 39 | 40 | public IEnumerable CreatePatches(IEnumerable edgePoints) { 41 | const double range = 1.6; 42 | foreach (var point in edgePoints) { 43 | var neighbourhood = this.hashSet.GetPointsInRange(point, range, true); 44 | var highest = neighbourhood.OrderByDescending(p => p.y).First(); 45 | var best = neighbourhood.Where(p => highest.y - p.y < range / 2.0).OrderBy(p => Math.Pow(p.x - point.x, 2.0) + Math.Pow(p.z - point.z, 2.0)).First(); 46 | 47 | const double minDistance = 2; 48 | const double pointSpacing = 0.7; 49 | if (best.y > point.y && best.y - point.y > minDistance) { 50 | int count = (int)Math.Floor((best - point).Length / pointSpacing); 51 | 52 | for (int j = 1; j <= count; j++) { 53 | yield return point + (best - point) * ((double)j / count); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /PointcloudTool/PointHashSet.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System; 4 | 5 | public class PointHashSet { 6 | 7 | private class Bucket { 8 | public readonly int X; 9 | public readonly int Z; 10 | 11 | public Bucket(int x, int z) { 12 | this.X = x; 13 | this.Z = z; 14 | } 15 | 16 | public override int GetHashCode() { 17 | return this.X * 10000 + this.Z; 18 | } 19 | 20 | public override bool Equals(object obj) { 21 | if (!(obj is Bucket)) { 22 | return false; 23 | } 24 | Bucket bucket = obj as Bucket; 25 | return bucket.X == this.X && bucket.Z == this.Z; 26 | } 27 | } 28 | 29 | public readonly double BucketSize; 30 | private readonly Dictionary> data; 31 | private Vector3[] points; 32 | 33 | private Dictionary heightMap; 34 | private Dictionary normalMap; 35 | 36 | public PointHashSet(double bucketSize, Vector3[] points) { 37 | this.BucketSize = bucketSize; 38 | this.data = new Dictionary>(); 39 | this.points = points; 40 | 41 | for (int i = 0; i < this.points.Length; i++) { 42 | this.add(this.points[i], i); 43 | } 44 | this.prepareHeightmap(); 45 | } 46 | 47 | private Bucket getBucket(Vector3 point) { 48 | return new Bucket((int)Math.Floor(point.x / this.BucketSize), (int)Math.Floor(point.z / this.BucketSize)); 49 | } 50 | 51 | private void add(Vector3 point, int index) { 52 | var bucket = this.getBucket(point); 53 | if (!this.data.ContainsKey(bucket)) { 54 | this.data[bucket] = new HashSet(); 55 | } 56 | this.data[bucket].Add(point); 57 | } 58 | 59 | public IEnumerable GetPointsInRange(Vector3 point, double radius, bool strict) { 60 | var lowerCorner = this.getBucket(new Vector3(point.x - radius, point.y, point.z - radius)); 61 | var upperCorner = this.getBucket(new Vector3(point.x + radius, point.y, point.z + radius)); 62 | double radiusSquared = Math.Pow(radius, 2.0f); 63 | 64 | for (int x = lowerCorner.X; x <= upperCorner.X; x++) { 65 | for (int z = lowerCorner.Z; z <= upperCorner.Z; z++) { 66 | var bucket = new Bucket(x, z); 67 | if (!this.data.ContainsKey(bucket)) { 68 | continue; 69 | } 70 | foreach (var pointInRadius in this.data[bucket]) { 71 | if (!strict || Math.Pow(pointInRadius.x - point.x, 2.0f) + Math.Pow(pointInRadius.z - point.z, 2.0f) < radiusSquared) { 72 | yield return pointInRadius; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | private void prepareHeightmap() { 80 | this.heightMap = new Dictionary(); 81 | this.normalMap = new Dictionary(); 82 | foreach (var bucket in this.data.Keys) { 83 | this.heightMap[bucket] = this.data[bucket].Max(v => v.y); 84 | } 85 | 86 | foreach (var bucket in this.data.Keys) { 87 | var buckets = new Bucket[] { 88 | bucket, 89 | new Bucket(bucket.X, bucket.Z + 1), 90 | new Bucket(bucket.X + 1, bucket.Z + 1), 91 | new Bucket(bucket.X + 1, bucket.Z) 92 | }; 93 | var points = buckets 94 | .Where(b => this.heightMap.ContainsKey(b)) 95 | .Select(b => this.getHeightmapPoint(b)) 96 | .ToArray(); 97 | if (points.Length < 3) { 98 | continue; 99 | } 100 | var normal = GetPlaneNormal(points); 101 | if (normal.y < 0) { 102 | normal = normal * -1; 103 | } 104 | this.normalMap[bucket] = normal; 105 | } 106 | } 107 | 108 | private Vector3 getHeightmapPoint(Bucket bucket) { 109 | return new Vector3(((double)bucket.X + 0.5) * this.BucketSize, this.heightMap[bucket], ((double)bucket.Z + 0.5) * this.BucketSize); 110 | } 111 | 112 | public Vector3[] GetHeightMap() { 113 | return this.heightMap.Keys.Select(bucket => this.getHeightmapPoint(bucket)).ToArray(); 114 | } 115 | 116 | public Vector3[] GetHeightMapNormals() { 117 | return this.heightMap.Keys.Select(bucket => this.normalMap.ContainsKey(bucket) ? this.normalMap[bucket] : new Vector3(0, 1, 0)).ToArray(); 118 | } 119 | 120 | public static Vector3 GetPlaneNormal(Vector3[] points) { 121 | // http://www.ilikebigbits.com/blog/2015/3/2/plane-from-points 122 | var centroid = points.Aggregate(new Vector3(0, 0, 0), (a, b) => a + b) / points.Length; 123 | 124 | double xx = 0, xy = 0, xz = 0, yy = 0, yz = 0, zz = 0; 125 | 126 | foreach (var point in points) { 127 | var relative = point - centroid; 128 | xx += relative.x * relative.y; 129 | xy += relative.x * relative.y; 130 | xz += relative.x * relative.z; 131 | yy += relative.y * relative.y; 132 | yz += relative.y * relative.z; 133 | zz += relative.z * relative.z; 134 | } 135 | 136 | double detX = yy * zz - yz * yz; 137 | double detY = xx * zz - xz * xz; 138 | double detZ = xx * yy - xy * xy; 139 | double detMax = Math.Max(detX, Math.Max(detY, detZ)); 140 | 141 | if (detMax == detX) { 142 | double a = (xz * yz - xy * zz) / detX; 143 | double b = (xy * yz - xz * yy) / detX; 144 | return new Vector3(1, a, b); 145 | } else if (detMax == detY) { 146 | double a = (yz * xz - xy * zz) / detY; 147 | double b = (xy * xz - yz * xx) / detY; 148 | return new Vector3(a, 1, b); 149 | } else { 150 | double a = (yz * xy - xz * yy) / detZ; 151 | double b = (xz * xy - yz * xx) / detZ; 152 | return new Vector3(a, b, 1); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /PointcloudTool/PointcloudTool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | public static class PointcloudTool { 7 | 8 | private static void extract(string inputFolder, string outputFile, double x, double y, double size) { 9 | var extractor = new SquareExtractor(x, y, size); 10 | Console.WriteLine("Reading all .xyz files in " + inputFolder + "..."); 11 | 12 | foreach (var file in new DirectoryInfo(inputFolder).GetFiles()) { 13 | Console.WriteLine(extractor.Count + " " + file.Name); 14 | if (file.Extension != ".xyz") { 15 | continue; 16 | } 17 | extractor.ProcessXYZFile(file); 18 | } 19 | 20 | var points = extractor.GetCenteredPoints(); 21 | Console.WriteLine("Writing output file..."); 22 | XYZFile.Write(outputFile, points); 23 | Console.WriteLine("Complete."); 24 | } 25 | 26 | private static void createPatches(Vector3[] points, string filename) { 27 | Console.WriteLine("Fixing holes..."); 28 | var fileWriter = new XYZFileWriter(filename, append: true); 29 | var holeFixer = new HoleFixer(points); 30 | 31 | var edgePoints = holeFixer.GetEdgePoints(); 32 | foreach (var point in holeFixer.CreatePatches(edgePoints)) { 33 | fileWriter.Write(point); 34 | } 35 | fileWriter.Close(); 36 | } 37 | 38 | private static void fix(string filename, string heightmapFile) { 39 | Console.WriteLine("Reading input file..."); 40 | var points = XYZFile.Read(filename); 41 | 42 | PointcloudTool.createPatches(points, filename); 43 | 44 | Console.WriteLine("Creating heightmap..."); 45 | var pointHashSet = new PointHashSet(1d, points); 46 | XYZFile.Write(heightmapFile, pointHashSet.GetHeightMap(), pointHashSet.GetHeightMapNormals()); 47 | 48 | Console.WriteLine("Complete."); 49 | } 50 | 51 | private static void makeSolid(string inputFile, string outputFile, string cubeFile, double size, double zMargin) { 52 | Console.WriteLine("Reading mesh..."); 53 | var meshCreator = new SolidMeshCreator(STLFile.Read(inputFile).ToArray(), size, zMargin); 54 | 55 | Console.WriteLine("Writing..."); 56 | STLFile.Write(outputFile, meshCreator.Triangles); 57 | 58 | STLFile.Write(cubeFile, meshCreator.GetCube().ToArray()); 59 | 60 | Console.WriteLine("Complete."); 61 | } 62 | 63 | private static void printUsage() { 64 | string name = System.AppDomain.CurrentDomain.FriendlyName; 65 | Console.WriteLine("This program can perform any one of three steps needed for mesh creation."); 66 | Console.WriteLine(name + " extract "); 67 | Console.WriteLine(name + " fix "); 68 | Console.WriteLine(name + " makeSolid "); 69 | } 70 | 71 | public static void Main(string[] args) { 72 | if (args.Length == 0) { 73 | printUsage(); 74 | return; 75 | } 76 | try { 77 | if (args[0] == "extract") { 78 | if (args.Length != 6) { 79 | printUsage(); 80 | return; 81 | } 82 | string inputFolder = args[1]; 83 | string outputFile = args[2]; 84 | double x = double.Parse(args[3], CultureInfo.InvariantCulture); 85 | double y = double.Parse(args[4], CultureInfo.InvariantCulture); 86 | double size = double.Parse(args[5], CultureInfo.InvariantCulture); 87 | 88 | PointcloudTool.extract(inputFolder, outputFile, x, y, size); 89 | } else if (args[0] == "fix") { 90 | if (args.Length != 3) { 91 | printUsage(); 92 | return; 93 | } 94 | 95 | string pointcloudFile = args[1]; 96 | string heightmapFile = args[2]; 97 | 98 | PointcloudTool.fix(pointcloudFile, heightmapFile); 99 | } else if (args[0].ToLower() == "makesolid") { 100 | if (args.Length != 6) { 101 | printUsage(); 102 | return; 103 | } 104 | 105 | string inputFile = args[1]; 106 | string outputFile = args[2]; 107 | string cubeFile = args[3]; 108 | double size = double.Parse(args[4], CultureInfo.InvariantCulture); 109 | double zMargin = double.Parse(args[5], CultureInfo.InvariantCulture); 110 | 111 | PointcloudTool.makeSolid(inputFile, outputFile, cubeFile, size, zMargin); 112 | } else { 113 | printUsage(); 114 | } 115 | } catch (Exception exception) { 116 | Console.WriteLine(exception); 117 | Environment.Exit(1); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /PointcloudTool/PointcloudTool.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {77FDE49B-5E66-4AED-8C3D-BF7874431C40} 8 | Exe 9 | Properties 10 | PointcloudTool 11 | PointcloudTool 12 | v4.5 13 | 512 14 | publish\ 15 | true 16 | Disk 17 | false 18 | Foreground 19 | 7 20 | Days 21 | false 22 | false 23 | true 24 | 0 25 | 1.0.0.%2a 26 | false 27 | false 28 | true 29 | 30 | 31 | true 32 | full 33 | false 34 | bin\Debug\ 35 | DEBUG;TRACE 36 | prompt 37 | 4 38 | false 39 | 40 | 41 | pdbonly 42 | true 43 | bin\Release\ 44 | TRACE 45 | prompt 46 | 4 47 | false 48 | 49 | 50 | 51 | 52 | 53 | true 54 | bin\x64\Debug\ 55 | DEBUG;TRACE 56 | full 57 | x64 58 | prompt 59 | MinimumRecommendedRules.ruleset 60 | true 61 | 62 | 63 | bin\x64\Release\ 64 | TRACE 65 | true 66 | pdbonly 67 | x64 68 | prompt 69 | MinimumRecommendedRules.ruleset 70 | true 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | False 97 | Microsoft .NET Framework 4.5 %28x86 and x64%29 98 | true 99 | 100 | 101 | False 102 | .NET Framework 3.5 SP1 Client Profile 103 | false 104 | 105 | 106 | False 107 | .NET Framework 3.5 SP1 108 | false 109 | 110 | 111 | 112 | 119 | -------------------------------------------------------------------------------- /PointcloudTool/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PointcloudTool")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PointcloudTool")] 13 | [assembly: AssemblyCopyright("")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("156e4794-88fb-4371-aae0-66b2d6fbb0e9")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /PointcloudTool/STLFile.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System; 5 | using System.Globalization; 6 | 7 | public static class STLFile { 8 | 9 | public static IEnumerable Read(string filename) { 10 | var stream = new FileStream(filename, FileMode.Open); 11 | var reader = new BinaryReader(stream); 12 | 13 | reader.ReadBytes(80); 14 | uint count = reader.ReadUInt32(); 15 | 16 | for (uint i = 0; i < count; i++) { 17 | reader.ReadBytes(4 * 3); // Normal 18 | var triangle = new Triangle( 19 | new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), 20 | new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), 21 | new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle())); 22 | if (!triangle.HasZeroArea()) { 23 | yield return triangle; 24 | } 25 | 26 | reader.ReadBytes(2); // Attribute count 27 | } 28 | reader.Close(); 29 | stream.Close(); 30 | } 31 | 32 | public static void Write(string filename, Triangle[] mesh) { 33 | var stream = new FileStream(filename, FileMode.Create); 34 | var writer = new BinaryWriter(stream); 35 | 36 | writer.Write(Enumerable.Repeat((byte)0, 80).ToArray()); 37 | writer.Write((uint)mesh.Length); 38 | 39 | foreach (var item in mesh) { 40 | writer.Write((float)item.Normal.x); 41 | writer.Write((float)item.Normal.y); 42 | writer.Write((float)item.Normal.z); 43 | writer.Write((float)item.V1.x); 44 | writer.Write((float)item.V1.y); 45 | writer.Write((float)item.V1.z); 46 | writer.Write((float)item.V2.x); 47 | writer.Write((float)item.V2.y); 48 | writer.Write((float)item.V2.z); 49 | writer.Write((float)item.V3.x); 50 | writer.Write((float)item.V3.y); 51 | writer.Write((float)item.V3.z); 52 | writer.Write(Enumerable.Repeat((byte)0, 2).ToArray()); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /PointcloudTool/SolidMeshCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | public class SolidMeshCreator { 7 | private readonly double size; 8 | private readonly double zMargin; 9 | 10 | private Vector3 boundingLower; 11 | private Vector3 boundingUpper; 12 | 13 | public Triangle[] Triangles { 14 | get; 15 | private set; 16 | } 17 | 18 | private Vector3[] seam; 19 | 20 | public SolidMeshCreator(Triangle[] triangles, double size, double zMargin) { 21 | this.Triangles = triangles; 22 | this.size = size; 23 | this.zMargin = zMargin; 24 | this.calculateBounds(); 25 | this.seam = this.getSeam(); 26 | this.createGeometry(); 27 | } 28 | 29 | private void calculateBounds() { 30 | double minX = Double.PositiveInfinity; 31 | double maxX = Double.NegativeInfinity; 32 | double minY = Double.PositiveInfinity; 33 | double maxY = Double.NegativeInfinity; 34 | double minZ = Double.PositiveInfinity; 35 | double maxZ = Double.NegativeInfinity; 36 | 37 | foreach (var triangle in this.Triangles) { 38 | foreach (var vertex in triangle.ToEnumerable()) { 39 | if (vertex.x < minX) minX = vertex.x; 40 | if (vertex.x > maxX) maxX = vertex.x; 41 | if (vertex.y < minY) minY = vertex.y; 42 | if (vertex.y > maxY) maxY = vertex.y; 43 | if (vertex.z < minZ) minZ = vertex.z; 44 | if (vertex.z > maxZ) maxZ = vertex.z; 45 | } 46 | } 47 | 48 | this.boundingLower = new Vector3(minX, minY, minZ); 49 | this.boundingUpper = new Vector3(maxX, maxY, maxZ); 50 | } 51 | 52 | private Vector3[] getSeam() { 53 | var right = new List(); 54 | var down = new List(); 55 | var left = new List(); 56 | var up = new List(); 57 | 58 | double margin = 0.01; 59 | 60 | foreach (var triangle in this.Triangles) { 61 | foreach (var vertex in triangle.ToEnumerable()) { 62 | if (vertex.x > this.boundingUpper.x - margin) { 63 | right.Add(vertex); 64 | } else if (vertex.y < this.boundingLower.y + margin) { 65 | down.Add(vertex); 66 | } else if (vertex.x < this.boundingLower.x + margin) { 67 | left.Add(vertex); 68 | } else if (vertex.y > this.boundingUpper.y - margin) { 69 | up.Add(vertex); 70 | } 71 | } 72 | } 73 | 74 | return right.OrderByDescending(v => v.y) 75 | .Concat(down.OrderByDescending(v => v.x)) 76 | .Concat(left.OrderBy(v => v.y)) 77 | .Concat(up.OrderBy(v => v.x)) 78 | .ToArray(); 79 | } 80 | 81 | private void createGeometry() { 82 | var result = new List(); 83 | 84 | double groundHeight = this.boundingLower.z - this.zMargin - 2; 85 | 86 | for (int i = 0; i < this.seam.Length; i++) { 87 | var a = this.seam[i]; 88 | var b = this.seam[(i + 1) % this.seam.Length]; 89 | var c = new Vector3(a.x, a.y, groundHeight); 90 | var d = new Vector3(b.x, b.y, groundHeight); 91 | 92 | result.Add(new Triangle(a, d, c)); 93 | result.Add(new Triangle(a, b, d)); 94 | } 95 | 96 | for (int i = 0; i < this.seam.Length / 2; i++) { 97 | var a = this.seam[i]; 98 | var b = this.seam[i + 1]; 99 | var c = this.seam[this.seam.Length - 1 - i]; 100 | var d = this.seam[this.seam.Length - 2 - i]; 101 | a = new Vector3(a.x, a.y, groundHeight); 102 | b = new Vector3(b.x, b.y, groundHeight); 103 | c = new Vector3(c.x, c.y, groundHeight); 104 | d = new Vector3(d.x, d.y, groundHeight); 105 | 106 | result.Add(new Triangle(c, a, d)); 107 | result.Add(new Triangle(a, b, d)); 108 | } 109 | 110 | this.Triangles = this.Triangles.Concat(result).ToArray(); 111 | } 112 | 113 | public IEnumerable GetCube() { 114 | double zLower = this.boundingLower.z - this.zMargin; 115 | double zUpper = this.boundingUpper.z + 2; 116 | double halfSize = size / 2; 117 | 118 | var a = new Vector3(+halfSize, -halfSize, zUpper); 119 | var b = new Vector3(+halfSize, +halfSize, zUpper); 120 | var c = new Vector3(-halfSize, +halfSize, zUpper); 121 | var d = new Vector3(-halfSize, -halfSize, zUpper); 122 | 123 | var e = new Vector3(+halfSize, -halfSize, zLower); 124 | var f = new Vector3(+halfSize, +halfSize, zLower); 125 | var g = new Vector3(-halfSize, +halfSize, zLower); 126 | var h = new Vector3(-halfSize, -halfSize, zLower); 127 | 128 | // up 129 | yield return new Triangle(a, b, c); 130 | yield return new Triangle(a, c, d); 131 | 132 | // front 133 | yield return new Triangle(c, b, f); 134 | yield return new Triangle(c, f, g); 135 | 136 | 137 | // left 138 | yield return new Triangle(d, c, g); 139 | yield return new Triangle(d, g, h); 140 | 141 | 142 | // right 143 | yield return new Triangle(b, a, e); 144 | yield return new Triangle(b, e, f); 145 | 146 | 147 | // back 148 | yield return new Triangle(a, d, h); 149 | yield return new Triangle(a, h, e); 150 | 151 | 152 | // down 153 | yield return new Triangle(h, g, f); 154 | yield return new Triangle(h, f, e); 155 | } 156 | } -------------------------------------------------------------------------------- /PointcloudTool/SquareExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | 8 | public class SquareExtractor { 9 | public readonly double CenterX; 10 | public readonly double CenterY; 11 | public readonly double halfSize; 12 | 13 | private Vector3 center; 14 | private double minDistance; 15 | 16 | private List points; 17 | 18 | public SquareExtractor(double centerX, double centerY, double size) { 19 | this.CenterX = centerX; 20 | this.CenterY = centerY; 21 | this.halfSize = size / 2.0; 22 | this.minDistance = Double.PositiveInfinity; 23 | this.points = new List(); 24 | } 25 | 26 | private void handlePoint(Vector3 point) { 27 | if (Math.Abs(point.x - this.CenterX) > halfSize || Math.Abs(point.z - this.CenterY) > halfSize) { 28 | return; 29 | } 30 | 31 | this.points.Add(point); 32 | double distance = Math.Pow(point.x - this.CenterX, 2.0) + Math.Pow(point.z - this.CenterY, 2.0); 33 | if (distance < this.minDistance) { 34 | this.minDistance = distance; 35 | this.center = point; 36 | } 37 | } 38 | 39 | public Vector3[] GetCenteredPoints() { 40 | return this.points.Select(p => new Vector3(p.x - this.center.x, p.y - this.center.y, p.z - this.center.z)).ToArray(); 41 | } 42 | 43 | private bool outOfRange(string filename) { 44 | if (!Regex.Match(filename, "dom1l-fp_32[0-9]*_[0-9]*_1_nw.xyz").Success) { 45 | return false; 46 | } 47 | 48 | var array = filename.Split('_'); 49 | double x = int.Parse(array[1].Substring(2)) * 1000; 50 | double y = int.Parse(array[2]) * 1000; 51 | 52 | return !(this.CenterX + this.halfSize > x 53 | && this.CenterX - halfSize < x + 1000 54 | && this.CenterY + this.halfSize > y 55 | && this.CenterY - halfSize < y + 1000); 56 | } 57 | 58 | public void ProcessXYZFile(FileInfo file) { 59 | if (this.outOfRange(file.Name)) { 60 | Console.WriteLine("Skipping " + file.Name + " since it contains an unrelated tile."); 61 | return; 62 | } 63 | 64 | foreach (var point in XYZFile.ReadContinuously(file.FullName)) { 65 | this.handlePoint(point); 66 | } 67 | } 68 | 69 | public int Count { 70 | get { 71 | return this.points.Count; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /PointcloudTool/Triangle.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | public class Triangle { 4 | public readonly Vector3 V1; 5 | public readonly Vector3 V2; 6 | public readonly Vector3 V3; 7 | 8 | public Vector3 Normal { 9 | get { 10 | var normal = Vector3.Cross(this.V2 - this.V1, this.V3 - this.V1); 11 | if (normal.y < 0) { 12 | return normal * -1f; 13 | } else { 14 | return normal; 15 | } 16 | } 17 | } 18 | 19 | public Triangle(Vector3 v1, Vector3 v2, Vector3 v3) { 20 | this.V1 = v1; 21 | this.V2 = v2; 22 | this.V3 = v3; 23 | } 24 | 25 | public bool HasZeroArea() { 26 | return this.V1.Equals(this.V2) || this.V2.Equals(this.V3) || this.V3.Equals(this.V1); 27 | } 28 | 29 | public IEnumerable ToEnumerable() { 30 | yield return this.V1; 31 | yield return this.V2; 32 | yield return this.V3; 33 | } 34 | 35 | public override string ToString() { 36 | return "Triangle: " + this.V1 + ", " + this.V2 + ", " + this.V3; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PointcloudTool/Vector3.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | public struct Vector3 { 5 | public readonly double x; 6 | public readonly double y; 7 | public readonly double z; 8 | 9 | public double Length { 10 | get { 11 | return Math.Sqrt(Math.Pow(this.x, 2.0) + Math.Pow(this.y, 2.0) + Math.Pow(this.z, 2.0)); 12 | } 13 | } 14 | 15 | public Vector3 Normalized { 16 | get { 17 | return this / this.Length; 18 | } 19 | } 20 | 21 | public Vector3(double x, double y, double z) { 22 | this.x = x; 23 | this.y = y; 24 | this.z = z; 25 | } 26 | 27 | public override string ToString() { 28 | return string.Format(CultureInfo.InvariantCulture, "({0:0.00} {1:0.00} {2:0.00})", this.x, this.y, this.z); 29 | } 30 | 31 | public static Vector3 operator +(Vector3 c1, Vector3 c2) { 32 | return new Vector3(c1.x + c2.x, c1.y + c2.y, c1.z + c2.z); 33 | } 34 | 35 | public static Vector3 operator -(Vector3 c1, Vector3 c2) { 36 | return new Vector3(c1.x - c2.x, c1.y - c2.y, c1.z - c2.z); 37 | } 38 | 39 | public static Vector3 operator *(Vector3 v, double f) { 40 | return new Vector3(v.x * f, v.y * f, v.z * f); 41 | } 42 | 43 | public static Vector3 operator *(double f, Vector3 v) { 44 | return new Vector3(v.x * f, v.y * f, v.z * f); 45 | } 46 | 47 | public static Vector3 operator /(Vector3 v, double f) { 48 | return new Vector3(v.x / f, v.y / f, v.z / f); 49 | } 50 | 51 | public override bool Equals(object obj) { 52 | if (!(obj is Vector3)) { 53 | return false; 54 | } 55 | Vector3 v = (Vector3)obj; 56 | return v.x == this.x && v.y == this.y && v.z == this.z; 57 | } 58 | 59 | public static double Dot(Vector3 a, Vector3 b) { 60 | return a.x * b.x + a.y * b.y + a.z * b.z; 61 | } 62 | 63 | public static Vector3 Cross(Vector3 a, Vector3 b) { 64 | return new Vector3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y + b.x); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /PointcloudTool/XYZFile.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System; 5 | using System.Globalization; 6 | 7 | public class XYZFile { 8 | private const int batchSize = 1000; 9 | 10 | public static Vector3[] Read(string fileName) { 11 | return XYZFile.ReadContinuously(fileName).ToArray(); 12 | } 13 | 14 | public static IEnumerable ReadContinuously(string fileName) { 15 | var filestream = new System.IO.FileStream(fileName, 16 | System.IO.FileMode.Open, 17 | System.IO.FileAccess.Read, 18 | System.IO.FileShare.ReadWrite); 19 | var streamReader = new System.IO.StreamReader(filestream, System.Text.Encoding.UTF8, true, 128); 20 | 21 | char separator = ','; 22 | bool hasCheckedSeparator = false; 23 | 24 | while (true) { 25 | var batch = readBatch(streamReader); 26 | if (!batch.Any()) { 27 | yield break; 28 | } 29 | foreach (var line in batch) { 30 | if (!hasCheckedSeparator) { 31 | separator = line.Contains(",") ? ',' : ' '; 32 | hasCheckedSeparator = false; 33 | } 34 | yield return parseLine(line, separator); 35 | } 36 | } 37 | } 38 | 39 | private static List readBatch(StreamReader reader) { 40 | var result = new List(); 41 | for (int i = 0; i < batchSize; i++) { 42 | string line = reader.ReadLine(); 43 | if (line == null) { 44 | break; 45 | } 46 | result.Add(line); 47 | } 48 | return result; 49 | } 50 | 51 | private static Vector3 parseLine(string line, char separator) { 52 | try { 53 | var points = line.Split(separator).Where(s => s.Any()).Select(s => double.Parse(s, CultureInfo.InvariantCulture)).ToArray(); 54 | return new Vector3(points[0], points[2], points[1]); 55 | } 56 | catch (FormatException) { 57 | throw new Exception("Bad line: " + line); 58 | } 59 | } 60 | 61 | public static void Write(string outputFile, IEnumerable points, char separator = ' ') { 62 | var fileWriter = new XYZFileWriter(outputFile, separator); 63 | fileWriter.Write(points); 64 | fileWriter.Close(); 65 | } 66 | 67 | public static void Write(string outputFile, Vector3[] points, Vector3[] normals, char separator = ' ') { 68 | var fileWriter = new XYZFileWriter(outputFile, separator); 69 | for (int i = 0; i < points.Length; i++) { 70 | var point = points[i]; 71 | var normal = normals[i]; 72 | 73 | fileWriter.Write(point, normal); 74 | } 75 | fileWriter.Close(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /PointcloudTool/XYZFileWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | 6 | class XYZFileWriter : IDisposable { 7 | private readonly StreamWriter streamWriter; 8 | private readonly char separator; 9 | 10 | public XYZFileWriter(string filename, char separator = ' ', bool append = false) { 11 | this.streamWriter = new StreamWriter(filename, append); 12 | this.separator = separator; 13 | } 14 | 15 | public void Write(Vector3 point) { 16 | this.streamWriter.WriteLine(string.Format(CultureInfo.InvariantCulture, 17 | "{0:0.00}{3}{1:0.00}{3}{2:0.00}", 18 | point.x, point.z, point.y, 19 | this.separator)); 20 | } 21 | 22 | public void Write(IEnumerable points) { 23 | foreach (var point in points) { 24 | this.Write(point); 25 | } 26 | } 27 | 28 | public void Write(Vector3 point, Vector3 normal) { 29 | this.streamWriter.WriteLine(string.Format(CultureInfo.InvariantCulture, 30 | "{0:0.00}{6}{1:0.00}{6}{2:0.00}{6}{3:0.00}{6}{4:0.00}{6}{5:0.00}", 31 | point.x, point.z, point.y, 32 | normal.x, normal.z, normal.y, 33 | this.separator)); 34 | } 35 | 36 | public void Dispose() { 37 | this.streamWriter.Dispose(); 38 | } 39 | 40 | public void Close() { 41 | this.streamWriter.Close(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pointcloudprinter 2 | A tool to turn pointcloud data from aerial lidar scans into solid meshes for 3D printing. 3 | You can find examples of meshes made with this software on [Thingiverse](https://www.thingiverse.com/thing:2993625). 4 | 5 | ![](https://i.imgur.com/LaZ5C9A.jpg) 6 | 7 | ## Deprecated project 8 | 9 | Since I published this project, the dataset I used is no longer available and the API in Meshlab that I used was dropped in a recent version of Meshlab. 10 | If you just found this project, it is very likely that it will be completely useless. 11 | I'm leaving it up in case people want to learn from it. 12 | If this project is still useful, please [let me know](mailto:mail@marian42.de). 13 | 14 | 15 | ## Requirements 16 | - Data: Your data needs to be one or multiple `.xyz` text files that contain comma separated numbers. I've tested this tool with data from [this website](https://www.opengeodata.nrw.de/produkte/geobasis/dom/dom1l/) which provides free data for the German state NRW. 17 | - A computer running Windows. (You can port this software to Linux though) 18 | - Have [Blender](https://www.blender.org/download/) and [Meshlab](http://www.meshlab.net/#download) installed. 19 | The current version was tested with Meshlab 2020.03 (It won't work with newer versions of Meshlab!) and Blender 2.82. 20 | 21 | ## Usage 22 | 1. Download and unpack [this software](https://github.com/marian42/pointcloudprinter/releases/download/1.3/pointcloudtool.zip). 23 | 2. Download your pointcloud data and move your `.xyz` files into the data folder. 24 | You can also put them somewhere else and configure the location later. 25 | 3. Decide on the location of the square you would like to extract from the data. 26 | I suggest you use [Google Maps](https://www.google.com/maps/) to find the right place. Copy the two numbers in your Google Maps URL. 27 | They are the latitude and longitude. 28 | 4. Find out what coordinate system your XYZ data use. 29 | Use a tool like [this](https://epsg.io/transform#s_srs=4326&t_srs=4647) to convert your coordinates to the same system that your data uses. 30 | The latitude and longitude are in the `EPSG:4326 WGS 84` format, which will be the input coordinate system in the transform coordinates app. 31 | 5. Edit the file `create_mesh.bat` and put in your configuration. 32 | You need to set your x and y coordinates of the center of the square you'd like to extract. 33 | You can also set the size the square. 34 | 6. Double click the `create_mesh.bat` file. 35 | It will now run all the steps required to generate the mesh. 36 | Depending on how much data there is to process, this will take between a few minutes and an hour. 37 | Once finished, the window closes and if everything worked, a file called `mesh.stl` can be found in the project directory. 38 | 39 | If you followed these steps and it did or did not work, please [tell me about it](mailto:mail@marian42.de)! 40 | 41 | ## How it works 42 | This paragraph will explain what each line of the batch file does. 43 | 44 | pointcloudtool.exe extract %datadirectory% pointcloud.xyz %x% %y% %size% 45 | 46 | This line runs the code from this repository to search all `.xyz`files in the `%datadirectory%`. It collects all points that are in the specified square and writes them to the file `pointcloud.xyz`. 47 | 48 | pointcloudtool.exe fix pointcloud.xyz pointcloud.xyz heightmap.xyz 49 | 50 | This line runs the same program to fix holes in the pointcloud by adding new points where there are holes. 51 | These holes confuse the mesh reconstruction algorithm later. It also creates a simplified heightmap of the pointcloud with normals. 52 | 53 | %meshlab% -i pointcloud.xyz -i heightmap.xyz -o mesh.stl -s filter_script.mlx 54 | 55 | This line runs Meshlab with a script that contains instructions on how to reconstruct the mesh. 56 | First, Meshlab runs a surface reconstruction algorithm on the heightmap. 57 | This can be done because normals where calculated for it earlier. 58 | Then, the normals of this mesh are transferred to the actual, high-detail pointcloud. 59 | The mesh reconstruction algorithm is run again, now on the big pointcloud. 60 | This mesh is then saved to mesh.stl. 61 | 62 | pointcloudtool.exe makeSolid mesh.stl mesh.stl cube.stl %size% %verticaloffset% 63 | 64 | This line, again, uses the program from this project. 65 | It adds new triangles around the seam of the mesh and connects them, resulting in a watertight, solid mesh. This could already be 3D-printed! 66 | Furthermore, it creates a box with the exact horizontal and vertical dimensions that where configured. 67 | 68 | %blender% -b -P intersect.py -- mesh.stl cube.stl mesh.stl 69 | 70 | This line uses Blender calculate the boolean intersection between the solid mesh and the box. The result of this is the final mesh. 71 | 72 | ## License 73 | 74 | This project is distributed under the MIT licensense. Exempt from the license are the parts that link a source, as I didn't write them. 75 | 76 | License for the preview image in this document: 77 | Land NRW (2018) 78 | Datenlizenz Deutschland - Namensnennung - Version 2.0 (www.govdata.de/dl-de/by-2-0) 79 | https://www.opengeodata.nrw.de/produkte/geobasis/dom/dom1l/ 80 | -------------------------------------------------------------------------------- /create_mesh.bat: -------------------------------------------------------------------------------- 1 | :: This script will search through pointcloud data in the data folder and generate a 3D printable model. 2 | :: Please update these parameters before using it: 3 | 4 | :: The center of the square to extract, using the same coordinate system as the XYZ data supplied. 5 | SET x=384257.488234335 6 | SET y=5686683.1372826 7 | 8 | :: The size of the square to extract, in meters 9 | SET size=200 10 | 11 | :: Add some space between the lowest part of the surface and the bottom of the 3D mesh. In meters. 12 | SET verticaloffset=4 13 | 14 | :: The directory where the program will look for .xyz files 15 | SET datadirectory=data 16 | 17 | :: Make sure Meshlab and Blender are installed and put their executable locations here. 18 | SET meshlab="C:\Program Files\VCG\MeshLab\meshlabserver.exe" 19 | SET blender="C:\Program Files\Blender Foundation\Blender 2.82\blender.exe" 20 | 21 | :: Nothing to configure below this. 22 | 23 | pointcloudtool.exe extract %datadirectory% pointcloud.xyz %x% %y% %size% || goto :error 24 | 25 | pointcloudtool.exe fix pointcloud.xyz heightmap.xyz || goto :error 26 | 27 | %meshlab% -i pointcloud.xyz heightmap.xyz -o mesh.stl -s filter_script.mlx || goto :error 28 | 29 | pointcloudtool.exe makeSolid mesh.stl mesh.stl cube.stl %size% %verticaloffset% || goto :error 30 | 31 | %blender% -b -P intersect.py -- mesh.stl cube.stl mesh.stl || goto :error 32 | 33 | del cube.stl 34 | del pointcloud.xyz 35 | del heightmap.xyz 36 | 37 | exit /b 38 | 39 | :error 40 | echo Failed to create the mesh. && pause -------------------------------------------------------------------------------- /filter_script.mlx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /intersect.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import sys 3 | 4 | index = sys.argv.index("--") 5 | file_a = sys.argv[index + 1] 6 | file_b = sys.argv[index + 2] 7 | file_out = sys.argv[index + 3] 8 | 9 | bpy.ops.object.select_all(action = 'SELECT') 10 | bpy.ops.object.delete() 11 | 12 | bpy.ops.import_mesh.stl(filepath = file_a) 13 | obj_a = bpy.context.selected_objects[0] 14 | 15 | bpy.ops.import_mesh.stl(filepath = file_b) 16 | obj_b = bpy.context.selected_objects[0] 17 | 18 | bpy.context.view_layer.objects.active = obj_a 19 | bpy.ops.object.modifier_add(type = 'BOOLEAN') 20 | bpy.context.object.modifiers[0].object = obj_b 21 | bpy.context.object.modifiers[0].operation = 'INTERSECT' 22 | bpy.ops.object.modifier_apply(modifier='Boolean') 23 | 24 | bpy.context.view_layer.objects.active = obj_b 25 | bpy.ops.object.delete() 26 | 27 | bpy.context.view_layer.objects.active = obj_a 28 | bpy.ops.export_mesh.stl(filepath = file_out) -------------------------------------------------------------------------------- /pointcloudtool.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D36F4D84-703A-481B-A660-D85038F13260}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PointcloudTool", "PointcloudTool\PointcloudTool.csproj", "{77FDE49B-5E66-4AED-8C3D-BF7874431C40}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Release|Any CPU = Release|Any CPU 15 | Release|x64 = Release|x64 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {77FDE49B-5E66-4AED-8C3D-BF7874431C40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {77FDE49B-5E66-4AED-8C3D-BF7874431C40}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {77FDE49B-5E66-4AED-8C3D-BF7874431C40}.Debug|x64.ActiveCfg = Debug|x64 21 | {77FDE49B-5E66-4AED-8C3D-BF7874431C40}.Debug|x64.Build.0 = Debug|x64 22 | {77FDE49B-5E66-4AED-8C3D-BF7874431C40}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {77FDE49B-5E66-4AED-8C3D-BF7874431C40}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {77FDE49B-5E66-4AED-8C3D-BF7874431C40}.Release|x64.ActiveCfg = Release|x64 25 | {77FDE49B-5E66-4AED-8C3D-BF7874431C40}.Release|x64.Build.0 = Release|x64 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | EndGlobal 31 | --------------------------------------------------------------------------------