CalibrationChecks.swift (5410B)
1 import Foundation 2 import simd 3 func check(_ ok: Bool, _ what: String) { print(ok ? "PASS" : "FAIL", what); if !ok { exit(1) } } 4 func near(_ a: Double, _ b: Double, _ eps: Double = 1e-9) -> Bool { abs(a - b) <= eps } 5 6 /// 3x3 identity mixing matrix, 4 temperature coefficients per entry (constant term first). 7 func identityMixModel() -> [Double] { 8 var v: [Double] = [] 9 for i in 0 ..< 9 { v += [i % 4 == 0 ? 1 : 0, 0, 0, 0] } 10 return v 11 } 12 13 /// Synthetic G1-like panel: 4320x2160, eye centers in each half, focal ~1000 px, mild positive k1. 14 func fixtureJSON(k1: Double, chroma: Double = 0) -> Data { 15 func display(_ eye: String, _ cx: Double, _ tx: Double) -> [String: Any] { 16 let f = 1000.0 17 let affine = [f, 0, cx, 0, f, 1080, 0, 0, 1] // tangent-plane -> pixels; inverse maps pixels -> tangents 18 func dist(_ k: Double) -> [String: Any] { 19 ["ModelType": "CALIBRATION_DisplayDistortionModelPolynomial3K", "ModelParameterCount": 5, 20 "ModelParameters": [cx, 1080, k, 0, 0]] 21 } 22 return ["AssignedEye": eye, "Affine": affine, "DisplayWidth": 4320, "DisplayHeight": 2160, 23 "VisibleAreaCenter": ["X": cx, "Y": 1080], "VisibleAreaRadius": 1050, 24 "Rt": ["Rotation": [1, 0, 0, 0, 1, 0, 0, 0, 1], "Translation": [tx, 0, 0]], 25 "DistortionRed": dist(k1 * (1 + chroma)), "DistortionGreen": dist(k1), "DistortionBlue": dist(k1 * (1 - chroma))] 26 } 27 let gyro: [String: Any] = ["SensorType": "CALIBRATION_InertialSensorType_Gyro", 28 "Rt": ["Rotation": [0, 1, 0, -1, 0, 0, 0, 0, 1], "Translation": [0, 0, 0]], 29 "BiasTemperatureModel": [0.01, 0, 0, 0, 0.02, 0, 0, 0, 0.03, 0, 0, 0], 30 "MixingMatrixTemperatureModel": identityMixModel()] 31 let root: [String: Any] = ["CalibrationInformation": [ 32 "Displays": [display("CALIBRATION_DisplayEyeLeft", 1080, -0.0315), display("CALIBRATION_DisplayEyeRight", 3240, 0.0315)], 33 "InertialSensors": [gyro], "Cameras": [] as [Any]]] 34 return try! JSONSerialization.data(withJSONObject: root) 35 } 36 37 @main 38 struct CalibrationChecks { 39 static func main() { 40 41 do { 42 check(wmrConfigKey.count == 1024 && wmrConfigKey.prefix(4) == [0x2F, 0xC8, 0x0F, 0x38], "key: 1024 bytes, upstream prefix") 43 check(WMRConfigProtocol.request(0x0b) == [0x02, 0x0b] + [UInt8](repeating: 0, count: 62), "request layout") 44 var chunk = [UInt8](repeating: 0, count: 33); chunk[0] = 2; chunk[1] = 1; chunk[2] = 3; chunk[3] = 7; chunk[4] = 8; chunk[5] = 9 45 check(WMRConfigProtocol.parseChunk(chunk) == .data([7, 8, 9]), "chunk reply parse") 46 chunk[1] = 2; check(WMRConfigProtocol.parseChunk(chunk) == .end, "end reply parse") 47 check(WMRConfigProtocol.dataSize(fromMetadata: [0x34, 0x12]) == 0x1234, "metadata size LE") 48 49 // 1. Block round trip: encode synthetic JSON like the headset store, decode it back. 50 let json = fixtureJSON(k1: 0) 51 let block = try WMRConfigBlock(raw: WMRConfigBlock.encodeForTesting(json: json)) 52 check(block.json == json && block.name == "Test Headset", "config block XOR/header round trip") 53 54 // 2. Parse and identity model: k = 0 and a pure scale affine => sample coords equal output coords. 55 let cal = try HeadsetCalibration(json: block.json) 56 check(cal.eyes.count == 2 && cal.sensors.count == 1 && cal.sensors[0].kind == .gyro, "parse displays + gyro") 57 check(near(EyeCalibration.ipd(cal.eyes[0], cal.eyes[1]), 0.063), "IPD from Rt translations") 58 check(near(cal.sensors[0].biasOffsets.y, 0.02), "gyro bias constant terms") 59 for view in 0 ..< 2 { 60 let eye = cal.eyes[view], b = eye.distortionBounds(view: view) 61 check(near(b.texXRange.x, -1.08) && near(b.texXRange.y, 1.08) && near(b.texYRange.x, -1.08) && near(b.texYRange.y, 1.08), 62 "view \(view): identity bounds = +-1080/1000") 63 for (u, v) in [(0.0, 0.0), (0.5, 0.5), (0.25, 0.8), (1.0, 1.0)] { 64 let s = eye.distort(view: view, u: u, v: v, bounds: b) 65 check(near(s.g.x, u, 1e-9) && near(s.g.y, v, 1e-9) && s.r == s.g && s.b == s.g, "view \(view) identity (\(u), \(v))") 66 } 67 } 68 69 // 3. Barrel/pincushion behaviour: k1 > 0 pushes edge samples outward, centre unchanged, symmetric. 70 let k1 = 1e-7 71 let cal2 = try HeadsetCalibration(json: fixtureJSON(k1: k1, chroma: 0.05)) 72 let eye = cal2.eyes[0], b = eye.distortionBounds(view: 0) 73 let centre = eye.distort(view: 0, u: 0.5, v: 0.5, bounds: b) 74 check(near(centre.g.x, 0.5) && near(centre.g.y, 0.5), "k1>0: centre maps to centre") 75 let edge = eye.distort(view: 0, u: 0.9, v: 0.5, bounds: b), mirror = eye.distort(view: 0, u: 0.1, v: 0.5, bounds: b) 76 check(near(edge.g.x + mirror.g.x, 1, 1e-9), "k1>0: left/right symmetric") 77 // Hand-computed expectation for u = 0.9: pix.x = 864, d = 1 + k1 * 864^2, tangent = 864 * d / 1000. 78 let d = 1 + k1 * 864 * 864, tan = 864 * d / 1000 79 check(near(edge.g.x, (tan - b.texXRange.x) / (b.texXRange.y - b.texXRange.x), 1e-12), "k1>0: matches hand calculation") 80 check(edge.r.x > edge.g.x && edge.b.x < edge.g.x, "chromatic: red samples farther out than green, blue nearer") 81 check(b.angleLeft < 0 && b.angleRight > 0 && b.angleUp > 0 && b.angleDown < 0, "bounds -> asymmetric FOV angles") 82 print("all calibration self-tests passed") 83 } catch { 84 print("FAIL", error); exit(1) 85 } 86 87 } 88 }