Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ public enum AttributeEstimationPipelineConstants {

/**
An attribute estimation pipeline that processes editable accessibility features to estimate their attributes.

- MARK:
The individual attribute calculation functions have a lot of redundant code to get the relevant properties from the prerequisite cache.
Find a way to streamline this.
*/
public class AttributeEstimationPipeline: ObservableObject {
public struct PrerequisiteCache: Sendable {
Expand All @@ -67,6 +71,10 @@ public class AttributeEstimationPipeline: ObservableObject {
public var meshTriangles: [MeshTriangle]? = nil
public var meshAlignedPlane: Plane? = nil
public var meshProjectedPlane: ProjectedPlane? = nil

/// Additional lazy properties for caching can be added here as needed.
public var surfaceNormalsGrid: SurfaceNormalsForPointsGrid? = nil

}

public var captureImageData: (any CaptureImageDataProtocol)?
Expand Down Expand Up @@ -261,6 +269,20 @@ public class AttributeEstimationPipeline: ObservableObject {
try accessibilityFeature.setAttributeValue(
surfaceIntegrityAttributeValue, for: .surfaceIntegrity, isCalculated: true
)
case .surfaceDisruption:
let surfaceDisruptionAttributeValue = try self.calculateSurfaceDisruption(
accessibilityFeature: accessibilityFeature
)
try accessibilityFeature.setAttributeValue(
surfaceDisruptionAttributeValue, for: .surfaceDisruption, isCalculated: true
)
case .heightFromGround:
let heightFromGroundAttributeValue = try self.calculateHeightFromGround(
accessibilityFeature: accessibilityFeature
)
try accessibilityFeature.setAttributeValue(
heightFromGroundAttributeValue, for: .heightFromGround, isCalculated: true
)
default:
continue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import SwiftUI
import CoreLocation
import PointNMapShaderTypes
import simd

public extension AttributeEstimationPipeline {
func calculateSurfaceIntegrity(
Expand Down Expand Up @@ -43,9 +44,10 @@ public extension AttributeEstimationPipeline {
let projectedPlane: ProjectedPlane = try self.prerequisiteCache.pointProjectedPlane ?? self.calculateProjectedPlane(
accessibilityFeature: accessibilityFeature, plane: alignedPlane
)
let surfaceNormalsGrid: SurfaceNormalsForPointsGrid = try surfaceNormalsProcessor.getSurfaceNormalsFromWorldPoints(
let surfaceNormalsGrid: SurfaceNormalsForPointsGrid = try self.prerequisiteCache.surfaceNormalsGrid ?? surfaceNormalsProcessor.getSurfaceNormalsFromWorldPoints(
worldPointsGrid: worldPointsGrid, plane: alignedPlane, projectedPlane: projectedPlane
)
self.prerequisiteCache.surfaceNormalsGrid = surfaceNormalsGrid
let surfaceIntegrityResults = try surfaceIntegrityProcessor.getIntegrityResultsFromImage(
worldPointsGrid: worldPointsGrid, plane: alignedPlane, surfaceNormalsForPointsGrid: surfaceNormalsGrid,
damageDetectionResults: damageDetectionResults, captureData: captureImageData
Expand Down Expand Up @@ -145,3 +147,151 @@ public extension AttributeEstimationPipeline {
return worstStatus
}
}

/**
Additional sub-attributes of surface-integrity
*/
/// Sub-attribute: Surface disruption
public extension AttributeEstimationPipeline {
func calculateSurfaceDisruption(
accessibilityFeature: any EditableAccessibilityFeatureProtocol
) throws -> AccessibilityFeatureAttribute.Value {
let isMeshEnabled: Bool = self.captureMeshData != nil
if isMeshEnabled {
return try calculateSurfaceDisruptionFromMesh(accessibilityFeature: accessibilityFeature)
}
return try calculateSurfaceDisruptionFromImage(accessibilityFeature: accessibilityFeature)
}

func calculateSurfaceDisruptionFromImage(
accessibilityFeature: any EditableAccessibilityFeatureProtocol
) throws -> AccessibilityFeatureAttribute.Value {
guard let captureImageData = self.captureImageData else {
throw AttributeEstimationPipelineError.missingCaptureData
}
guard let surfaceNormalsProcessor = self.surfaceNormalsProcessor else {
throw AttributeEstimationPipelineError.missingPreprocessors
}
guard let surfaceIntegrityProcessor = self.surfaceIntegrityProcessor else {
throw AttributeEstimationPipelineError.missingPreprocessors
}
let damageDetectionResults = try getDamageDetectionResults(accessibilityFeature: accessibilityFeature)
let worldPointsGrid = try self.prerequisiteCache.worldPointsGrid ?? self.getWorldPointsGrid(accessibilityFeature: accessibilityFeature)
let worldPoints: [WorldPoint] = try self.prerequisiteCache.worldPoints ?? self.getWorldPoints(
accessibilityFeature: accessibilityFeature
)
let alignedPlane: Plane = try self.prerequisiteCache.pointAlignedPlane ?? self.calculateAlignedPlane(
accessibilityFeature: accessibilityFeature, worldPoints: worldPoints
)
let projectedPlane: ProjectedPlane = try self.prerequisiteCache.pointProjectedPlane ?? self.calculateProjectedPlane(
accessibilityFeature: accessibilityFeature, plane: alignedPlane
)
let surfaceNormalsGrid: SurfaceNormalsForPointsGrid = try self.prerequisiteCache.surfaceNormalsGrid ?? surfaceNormalsProcessor.getSurfaceNormalsFromWorldPoints(
worldPointsGrid: worldPointsGrid, plane: alignedPlane, projectedPlane: projectedPlane
)
self.prerequisiteCache.surfaceNormalsGrid = surfaceNormalsGrid
let (deviantPoints, validPoints) = try surfaceIntegrityProcessor.getSurfaceNormalIntegrityValueFromImage(
worldPointsGrid: worldPointsGrid, plane: alignedPlane, surfaceNormalsForPointsGrid: surfaceNormalsGrid,
damageDetectionResults: damageDetectionResults, captureData: captureImageData
)
let deviantPointProportion = validPoints > 0 ? deviantPoints / validPoints : 0

guard let surfaceDisruptionAttributeValue = AccessibilityFeatureAttribute.surfaceDisruption.value(from: deviantPointProportion) else {
throw AttributeEstimationPipelineError.attributeAssignmentError
}
return surfaceDisruptionAttributeValue
}

func calculateSurfaceDisruptionFromMesh(
accessibilityFeature: any EditableAccessibilityFeatureProtocol
) throws -> AccessibilityFeatureAttribute.Value {
guard let captureMeshData = self.captureMeshData else {
throw AttributeEstimationPipelineError.missingCaptureData
}
guard let surfaceIntegrityProcessor = self.surfaceIntegrityProcessor else {
throw AttributeEstimationPipelineError.missingPreprocessors
}
let damageDetectionResults = try getDamageDetectionResults(accessibilityFeature: accessibilityFeature)
let meshContents: MeshContents = try self.prerequisiteCache.meshContents ?? self.getMeshContents(
accessibilityFeature: accessibilityFeature
)
let meshPolygons: [MeshPolygon] = self.prerequisiteCache.meshPolygons ?? meshContents.polygons
let meshTriangles: [MeshTriangle] = self.prerequisiteCache.meshTriangles ?? meshContents.triangles
let alignedPlane: Plane = try self.prerequisiteCache.meshAlignedPlane ?? self.calculateAlignedPlane(
accessibilityFeature: accessibilityFeature, meshPolygons: meshPolygons
)
let (totalDeviantPolygons, totalValidPolygons) = try surfaceIntegrityProcessor.getSurfaceNormalIntegrityValueFromMesh(
meshTriangles: meshTriangles, plane: alignedPlane,
damageDetectionResults: damageDetectionResults, captureData: captureMeshData
)
let deviantPolygonProportion = totalValidPolygons > 0 ? totalDeviantPolygons / totalValidPolygons : 0

guard let surfaceDisruptionAttributeValue = AccessibilityFeatureAttribute.surfaceDisruption.value(from: deviantPolygonProportion) else {
throw AttributeEstimationPipelineError.attributeAssignmentError
}
return surfaceDisruptionAttributeValue
}
}

/// Sub-attribute: Height from ground
extension AttributeEstimationPipeline {
func calculateHeightFromGround(
accessibilityFeature: any EditableAccessibilityFeatureProtocol
) throws -> AccessibilityFeatureAttribute.Value {
let isMeshEnabled: Bool = self.captureMeshData != nil
if isMeshEnabled {
return try calculateHeightFromGroundFromMesh(accessibilityFeature: accessibilityFeature)
}
return try calculateHeightFromGroundFromImage(accessibilityFeature: accessibilityFeature)
}

func calculateHeightFromGroundFromImage(
accessibilityFeature: any EditableAccessibilityFeatureProtocol
) throws -> AccessibilityFeatureAttribute.Value {
guard let captureImageData = self.captureImageData else {
throw AttributeEstimationPipelineError.missingCaptureData
}
let worldPoints: [WorldPoint] = try self.prerequisiteCache.worldPoints ?? self.getWorldPoints(
accessibilityFeature: accessibilityFeature
)
let alignedPlane: Plane = try self.prerequisiteCache.pointAlignedPlane ?? self.calculateAlignedPlane(
accessibilityFeature: accessibilityFeature, worldPoints: worldPoints
)
let heightFromGround = getHeightFromGround(plane: alignedPlane, cameraTransform: captureImageData.cameraTransform)

guard let heightFromGroundAttributeValue = AccessibilityFeatureAttribute.heightFromGround.value(from: heightFromGround) else {
throw AttributeEstimationPipelineError.attributeAssignmentError
}
return heightFromGroundAttributeValue
}

func calculateHeightFromGroundFromMesh(
accessibilityFeature: any EditableAccessibilityFeatureProtocol
) throws -> AccessibilityFeatureAttribute.Value {
guard let captureMeshData = self.captureMeshData else {
throw AttributeEstimationPipelineError.missingCaptureData
}
let meshContents: MeshContents = try self.prerequisiteCache.meshContents ?? self.getMeshContents(
accessibilityFeature: accessibilityFeature
)
let meshPolygons: [MeshPolygon] = self.prerequisiteCache.meshPolygons ?? meshContents.polygons
let alignedPlane: Plane = try self.prerequisiteCache.meshAlignedPlane ?? self.calculateAlignedPlane(
accessibilityFeature: accessibilityFeature, meshPolygons: meshPolygons
)
let heightFromGround = getHeightFromGround(plane: alignedPlane, cameraTransform: captureMeshData.cameraTransform)

guard let heightFromGroundAttributeValue = AccessibilityFeatureAttribute.heightFromGround.value(from: heightFromGround) else {
throw AttributeEstimationPipelineError.attributeAssignmentError
}
return heightFromGroundAttributeValue
}

private func getHeightFromGround(plane: Plane, cameraTransform: simd_float4x4) -> Double {
let planeNormal = plane.normalVector
let planeOrigin = plane.origin
let userPosition4 = cameraTransform.columns.3
let userPosition = SIMD3<Float>(userPosition4.x, userPosition4.y, userPosition4.z)
let vectorFromPlaneToUser = userPosition - planeOrigin
return Double(simd_dot(vectorFromPlaneToUser, planeNormal))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C
case runningSlope
case crossSlope
case surfaceIntegrity
case surfaceDisruption
case heightFromGround
/**
- NOTE:
Experimental attributes
Expand All @@ -38,13 +40,15 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C
public enum ValueType: Sendable, Codable, Equatable {
case length
case angle
case number
case flag
case categorical(typeID: String)
}

public enum Value: Sendable, Codable, Equatable {
case length(Measurement<UnitLength>)
case angle(Measurement<UnitAngle>)
case number(Double)
case flag(Bool)
case categorical(AnyCategoricalValue)

Expand All @@ -54,6 +58,8 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C
return l1 == l2
case (.angle(let a1), .angle(let a2)):
return a1 == a2
case (.number(let n1), .number(let n2)):
return n1 == n2
case (.flag(let f1), .flag(let f2)):
return f1 == f2
case (.categorical(let c1), .categorical(let c2)):
Expand Down Expand Up @@ -93,6 +99,16 @@ public enum AccessibilityFeatureAttribute: String, Identifiable, CaseIterable, C
id: 40, name: "Surface Integrity", unit: nil,
valueType: .categorical(typeID: SurfaceIntegrityStatus.typeID),
)
case .surfaceDisruption:
return Metadata(
id: 45, name: "Surface Disruption", unit: nil,
valueType: .number
)
case .heightFromGround:
return Metadata(
id: 48, name: "Height from Ground", unit: UnitLength.meters,
valueType: .length,
)
case .lidarDepth:
return Metadata(
id: 50, name: "LiDAR Depth", unit: UnitLength.meters,
Expand Down Expand Up @@ -175,6 +191,7 @@ public extension AccessibilityFeatureAttribute.Value {
switch self {
case .length: return .length
case .angle: return .angle
case .number: return .number
case .flag: return .flag
case .categorical(let categoricalValue): return .categorical(typeID: categoricalValue.typeID)
}
Expand All @@ -192,6 +209,7 @@ public extension AccessibilityFeatureAttribute {
switch (self.valueType, value) {
case (.length, .length),
(.angle, .angle),
(.number, .number),
(.flag, .flag):
return true
case (.categorical(let expectedID), .categorical(let cat)):
Expand All @@ -212,6 +230,8 @@ public extension AccessibilityFeatureAttribute.Value {
return measurement.converted(to: .meters).value
case .angle(let measurement):
return measurement.converted(to: .degrees).value
case .number(let value):
return value
case .flag:
return nil
case .categorical:
Expand All @@ -234,6 +254,8 @@ public extension AccessibilityFeatureAttribute.Value {
return String(format: "%.2f", measurement.converted(to: .meters).value)
case .angle(let measurement):
return String(format: "%.2f", measurement.converted(to: .degrees).value)
case .number(let value):
return String(format: "%.2f", value)
case .flag(let value):
return value ? "yes" : "no"
case .categorical(let value):
Expand All @@ -249,6 +271,8 @@ public extension AccessibilityFeatureAttribute {
return .length(Measurement(value: double, unit: .meters))
case .angle:
return .angle(Measurement(value: double, unit: .degrees))
case .number:
return .number(double)
case .flag:
return nil // Flags cannot be represented as doubles
case .categorical:
Expand Down Expand Up @@ -327,6 +351,10 @@ public extension AccessibilityFeatureAttribute {
return String(format: "%.2f", measurement.converted(to: .degrees).value)
case (.surfaceIntegrity, .categorical(let categoricalValue)):
return categoricalValue.rawValue
case (.surfaceDisruption, .number(let value)):
return String(format: "%.2f", value)
case (.heightFromGround, .length(let measurement)):
return String(format: "%.2f", measurement.converted(to: .meters).value)
case (.lidarDepth, .length(let measurement)):
return String(format: "%.2f", measurement.converted(to: .meters).value)
case (.latitudeDelta, .length(let measurement)):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public enum AccessibilityFeatureKind: String, Identifiable, Codable, CaseIterabl
switch self {
case .sidewalk: return [
.width, .runningSlope, .crossSlope, .surfaceIntegrity,
.surfaceDisruption, .heightFromGround,
.widthLegacy, .runningSlopeLegacy, .crossSlopeLegacy,
.widthFromImage, .runningSlopeFromImage, .crossSlopeFromImage
]
Expand Down
Loading