From da2c304db8674e582fa71f275352d0cfbc1359e4 Mon Sep 17 00:00:00 2001 From: Christopher Remde Date: Wed, 10 Jun 2020 13:59:09 +0200 Subject: [PATCH 1/6] Added Temporal Sync functionality --- LiveScanServer/KinectServer.cs | 18 ++ LiveScanServer/KinectSettings.cs | 2 + LiveScanServer/KinectSocket.cs | 67 +++- LiveScanServer/SettingsForm.Designer.cs | 409 ++++++++++++------------ LiveScanServer/SettingsForm.cs | 16 + LiveScanServer/SettingsForm.resx | 3 + include/LiveScanClient/liveScanClient.h | 5 + include/LiveScanClient/utils.h | 7 +- src/LiveScanClient/liveScanClient.cpp | 28 ++ 9 files changed, 338 insertions(+), 217 deletions(-) diff --git a/LiveScanServer/KinectServer.cs b/LiveScanServer/KinectServer.cs index 6454822..4f9045c 100644 --- a/LiveScanServer/KinectServer.cs +++ b/LiveScanServer/KinectServer.cs @@ -232,6 +232,19 @@ namespace KinectServer } } + public void SendTemporalSyncData() + { + lock (oClientSocketLock) + { + lClientSockets[0].SendTemporalSyncStatus(true, false); + + for (int i = 1; i < lClientSockets.Count; i++) + { + lClientSockets[i].SendTemporalSyncStatus(false, true); + } + } + } + public bool GetStoredFrame(List> lFramesRGB, List> lFramesVerts) { bool bNoMoreStoredFrames; @@ -436,6 +449,11 @@ namespace KinectServer lClientSockets[i].bLatestFrameReceived = true; } + else if (buffer[0] == 4) + { + lClientSockets[i].ReceiveTemporalSyncStatus(); + } + buffer = lClientSockets[i].Receive(1); } } diff --git a/LiveScanServer/KinectSettings.cs b/LiveScanServer/KinectSettings.cs index 3627eb3..987e845 100644 --- a/LiveScanServer/KinectSettings.cs +++ b/LiveScanServer/KinectSettings.cs @@ -42,6 +42,8 @@ namespace KinectServer public bool bMergeScansForSave = true; public bool bSaveAsBinaryPLY = true; + public bool bEnableSync = false; + public KinectSettings() { aMinBounds[0] = -5f; diff --git a/LiveScanServer/KinectSocket.cs b/LiveScanServer/KinectSocket.cs index 32c9ca8..065a9a0 100644 --- a/LiveScanServer/KinectSocket.cs +++ b/LiveScanServer/KinectSocket.cs @@ -33,6 +33,8 @@ namespace KinectServer public bool bStoredFrameReceived = false; public bool bNoMoreStoredFrames = true; public bool bCalibrated = false; + public bool bIsMaster = false; + public bool bIsSubOrdinate = false; //The pose of the sensor in the scene (used by the OpenGLWindow to show the sensor) public AffineTransform oCameraPose = new AffineTransform(); //The transform that maps the vertices in the sensor coordinate system to the world corrdinate system. @@ -121,6 +123,23 @@ namespace KinectServer SendByte(); } + public void SendTemporalSyncStatus(bool isMaster, bool isSubOrdinate) + { + if (isMaster) + { + byteToSend[0] = 7; + SendByte(); + return; + } + + if (isSubOrdinate) + { + byteToSend[0] = 8; + SendByte(); + return; + } + } + public void ReceiveCalibrationData() { bCalibrated = true; @@ -148,6 +167,37 @@ namespace KinectServer UpdateSocketState(); } + public void ReceiveTemporalSyncStatus() + { + int nToRead; + byte[] buffer = new byte[1024]; + + while (oSocket.Available == 0) + { + if (!SocketConnected()) + return; + } + + oSocket.Receive(buffer, 4, SocketFlags.None); + nToRead = BitConverter.ToInt32(buffer, 0); + + if (nToRead == -1) + { + bIsSubOrdinate = true; + bIsMaster = false; + return; + } + + if(nToRead == 0) + { + bIsMaster = true; + bIsSubOrdinate = false; + return; + } + + UpdateSocketState(); + } + public void ReceiveFrame() { lFrameRGB.Clear(); @@ -293,10 +343,19 @@ namespace KinectServer public void UpdateSocketState() { - if (bCalibrated) - sSocketState = oSocket.RemoteEndPoint.ToString() + " Calibrated = true"; - else - sSocketState = oSocket.RemoteEndPoint.ToString() + " Calibrated = false"; + string tempSyncStatus = ""; + + if (bIsMaster) + { + tempSyncStatus = "[Master]"; + } + + if (bIsSubOrdinate) + { + tempSyncStatus = "[Subordinate]"; + } + + sSocketState = oSocket.RemoteEndPoint.ToString() + " Calibrated = " + bCalibrated + " " + tempSyncStatus; if (eChanged != null) eChanged(); diff --git a/LiveScanServer/SettingsForm.Designer.cs b/LiveScanServer/SettingsForm.Designer.cs index 0a4a91b..c8b4a6c 100644 --- a/LiveScanServer/SettingsForm.Designer.cs +++ b/LiveScanServer/SettingsForm.Designer.cs @@ -28,11 +28,14 @@ /// private void InitializeComponent() { + System.Windows.Forms.Label lbSyncWarning; this.lbMerge = new System.Windows.Forms.Label(); this.chMerge = new System.Windows.Forms.CheckBox(); this.lbICPIters = new System.Windows.Forms.Label(); this.txtICPIters = new System.Windows.Forms.TextBox(); this.grClient = new System.Windows.Forms.GroupBox(); + this.grSync = new System.Windows.Forms.GroupBox(); + this.chSync = new System.Windows.Forms.CheckBox(); this.grBody = new System.Windows.Forms.GroupBox(); this.chSkeletons = new System.Windows.Forms.CheckBox(); this.chBodyData = new System.Windows.Forms.CheckBox(); @@ -72,14 +75,16 @@ this.lbFilterDistance = new System.Windows.Forms.Label(); this.txtFilterDistance = new System.Windows.Forms.TextBox(); this.grServer = new System.Windows.Forms.GroupBox(); + this.cbCompressionLevel = new System.Windows.Forms.ComboBox(); + this.label2 = new System.Windows.Forms.Label(); this.rBinaryPly = new System.Windows.Forms.RadioButton(); this.lbFormat = new System.Windows.Forms.Label(); this.rAsciiPly = new System.Windows.Forms.RadioButton(); this.txtRefinIters = new System.Windows.Forms.TextBox(); this.lbOuterIters = new System.Windows.Forms.Label(); - this.label2 = new System.Windows.Forms.Label(); - this.cbCompressionLevel = new System.Windows.Forms.ComboBox(); + lbSyncWarning = new System.Windows.Forms.Label(); this.grClient.SuspendLayout(); + this.grSync.SuspendLayout(); this.grBody.SuspendLayout(); this.grMarkers.SuspendLayout(); this.grBounding.SuspendLayout(); @@ -90,20 +95,18 @@ // lbMerge // this.lbMerge.AutoSize = true; - this.lbMerge.Location = new System.Drawing.Point(7, 69); - this.lbMerge.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbMerge.Location = new System.Drawing.Point(5, 45); this.lbMerge.Name = "lbMerge"; - this.lbMerge.Size = new System.Drawing.Size(99, 20); + this.lbMerge.Size = new System.Drawing.Size(69, 13); this.lbMerge.TabIndex = 22; this.lbMerge.Text = "Scan saving:"; // // chMerge // this.chMerge.AutoSize = true; - this.chMerge.Location = new System.Drawing.Point(156, 70); - this.chMerge.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.chMerge.Location = new System.Drawing.Point(104, 45); this.chMerge.Name = "chMerge"; - this.chMerge.Size = new System.Drawing.Size(126, 24); + this.chMerge.Size = new System.Drawing.Size(86, 17); this.chMerge.TabIndex = 23; this.chMerge.Text = "merge scans"; this.chMerge.UseVisualStyleBackColor = true; @@ -112,46 +115,74 @@ // lbICPIters // this.lbICPIters.AutoSize = true; - this.lbICPIters.Location = new System.Drawing.Point(7, 34); - this.lbICPIters.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbICPIters.Location = new System.Drawing.Point(5, 22); this.lbICPIters.Name = "lbICPIters"; - this.lbICPIters.Size = new System.Drawing.Size(128, 20); + this.lbICPIters.Size = new System.Drawing.Size(86, 13); this.lbICPIters.TabIndex = 24; this.lbICPIters.Text = "Num of ICP iters:"; // // txtICPIters // - this.txtICPIters.Location = new System.Drawing.Point(156, 31); - this.txtICPIters.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtICPIters.Location = new System.Drawing.Point(104, 20); this.txtICPIters.Name = "txtICPIters"; - this.txtICPIters.Size = new System.Drawing.Size(55, 26); + this.txtICPIters.Size = new System.Drawing.Size(38, 20); this.txtICPIters.TabIndex = 25; this.txtICPIters.TextChanged += new System.EventHandler(this.txtICPIters_TextChanged); // // grClient // + this.grClient.Controls.Add(this.grSync); this.grClient.Controls.Add(this.grBody); this.grClient.Controls.Add(this.grMarkers); this.grClient.Controls.Add(this.grBounding); this.grClient.Controls.Add(this.grFiltering); - this.grClient.Location = new System.Drawing.Point(18, 18); - this.grClient.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.grClient.Location = new System.Drawing.Point(12, 12); this.grClient.Name = "grClient"; - this.grClient.Padding = new System.Windows.Forms.Padding(4, 5, 4, 5); - this.grClient.Size = new System.Drawing.Size(948, 447); + this.grClient.Size = new System.Drawing.Size(632, 291); this.grClient.TabIndex = 43; this.grClient.TabStop = false; this.grClient.Text = "KinectClient settings"; + // + // grSync + // + this.grSync.Controls.Add(lbSyncWarning); + this.grSync.Controls.Add(this.chSync); + this.grSync.Location = new System.Drawing.Point(271, 211); + this.grSync.Name = "grSync"; + this.grSync.Size = new System.Drawing.Size(355, 66); + this.grSync.TabIndex = 48; + this.grSync.TabStop = false; + this.grSync.Text = "Temporal Sync (Azure Kinect Only)"; + // + // lbSyncWarning + // + lbSyncWarning.AutoSize = true; + lbSyncWarning.CausesValidation = false; + lbSyncWarning.Location = new System.Drawing.Point(3, 41); + lbSyncWarning.Name = "lbSyncWarning"; + lbSyncWarning.Size = new System.Drawing.Size(347, 13); + lbSyncWarning.TabIndex = 1; + lbSyncWarning.Text = "Notice: Devices need to be connected via sync cabel before enabeling!\r\n"; + // + // chSync + // + this.chSync.AutoSize = true; + this.chSync.Location = new System.Drawing.Point(6, 19); + this.chSync.Name = "chSync"; + this.chSync.Size = new System.Drawing.Size(133, 17); + this.chSync.TabIndex = 0; + this.chSync.Text = "Enable Temporal Sync"; + this.chSync.UseVisualStyleBackColor = true; + this.chSync.CheckedChanged += new System.EventHandler(this.chSync_CheckedChanged); + // // grBody // this.grBody.Controls.Add(this.chSkeletons); this.grBody.Controls.Add(this.chBodyData); - this.grBody.Location = new System.Drawing.Point(14, 325); - this.grBody.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.grBody.Location = new System.Drawing.Point(9, 211); this.grBody.Name = "grBody"; - this.grBody.Padding = new System.Windows.Forms.Padding(4, 5, 4, 5); - this.grBody.Size = new System.Drawing.Size(374, 101); + this.grBody.Size = new System.Drawing.Size(249, 66); this.grBody.TabIndex = 47; this.grBody.TabStop = false; this.grBody.Text = "Body data"; @@ -159,10 +190,9 @@ // chSkeletons // this.chSkeletons.AutoSize = true; - this.chSkeletons.Location = new System.Drawing.Point(16, 63); - this.chSkeletons.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.chSkeletons.Location = new System.Drawing.Point(11, 41); this.chSkeletons.Name = "chSkeletons"; - this.chSkeletons.Size = new System.Drawing.Size(220, 24); + this.chSkeletons.Size = new System.Drawing.Size(154, 17); this.chSkeletons.TabIndex = 21; this.chSkeletons.Text = "show skeletons in live view"; this.chSkeletons.UseVisualStyleBackColor = true; @@ -171,10 +201,9 @@ // chBodyData // this.chBodyData.AutoSize = true; - this.chBodyData.Location = new System.Drawing.Point(16, 29); - this.chBodyData.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.chBodyData.Location = new System.Drawing.Point(11, 19); this.chBodyData.Name = "chBodyData"; - this.chBodyData.Size = new System.Drawing.Size(167, 24); + this.chBodyData.Size = new System.Drawing.Size(113, 17); this.chBodyData.TabIndex = 19; this.chBodyData.Text = "stream only bodies"; this.chBodyData.UseVisualStyleBackColor = true; @@ -198,11 +227,9 @@ this.grMarkers.Controls.Add(this.txtTranslationX); this.grMarkers.Controls.Add(this.lisMarkers); this.grMarkers.Controls.Add(this.txtTranslationY); - this.grMarkers.Location = new System.Drawing.Point(406, 29); - this.grMarkers.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.grMarkers.Location = new System.Drawing.Point(271, 19); this.grMarkers.Name = "grMarkers"; - this.grMarkers.Padding = new System.Windows.Forms.Padding(4, 5, 4, 5); - this.grMarkers.Size = new System.Drawing.Size(530, 285); + this.grMarkers.Size = new System.Drawing.Size(353, 185); this.grMarkers.TabIndex = 45; this.grMarkers.TabStop = false; this.grMarkers.Text = "Calibration markers"; @@ -210,19 +237,17 @@ // lbX2 // this.lbX2.AutoSize = true; - this.lbX2.Location = new System.Drawing.Point(345, 29); - this.lbX2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbX2.Location = new System.Drawing.Point(230, 19); this.lbX2.Name = "lbX2"; - this.lbX2.Size = new System.Drawing.Size(20, 20); + this.lbX2.Size = new System.Drawing.Size(14, 13); this.lbX2.TabIndex = 49; this.lbX2.Text = "X"; // // btRemove // - this.btRemove.Location = new System.Drawing.Point(350, 195); - this.btRemove.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.btRemove.Location = new System.Drawing.Point(233, 127); this.btRemove.Name = "btRemove"; - this.btRemove.Size = new System.Drawing.Size(138, 35); + this.btRemove.Size = new System.Drawing.Size(92, 23); this.btRemove.TabIndex = 59; this.btRemove.Text = "Remove marker"; this.btRemove.UseVisualStyleBackColor = true; @@ -230,76 +255,68 @@ // // txtOrientationZ // - this.txtOrientationZ.Location = new System.Drawing.Point(458, 58); - this.txtOrientationZ.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtOrientationZ.Location = new System.Drawing.Point(305, 38); this.txtOrientationZ.Name = "txtOrientationZ"; - this.txtOrientationZ.Size = new System.Drawing.Size(55, 26); + this.txtOrientationZ.Size = new System.Drawing.Size(38, 20); this.txtOrientationZ.TabIndex = 48; this.txtOrientationZ.TextChanged += new System.EventHandler(this.txtOrientationZ_TextChanged); // // txtId // - this.txtId.Location = new System.Drawing.Point(326, 143); - this.txtId.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtId.Location = new System.Drawing.Point(217, 93); this.txtId.Name = "txtId"; - this.txtId.Size = new System.Drawing.Size(55, 26); + this.txtId.Size = new System.Drawing.Size(38, 20); this.txtId.TabIndex = 58; this.txtId.TextChanged += new System.EventHandler(this.txtId_TextChanged); // // lbY2 // this.lbY2.AutoSize = true; - this.lbY2.Location = new System.Drawing.Point(411, 29); - this.lbY2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbY2.Location = new System.Drawing.Point(274, 19); this.lbY2.Name = "lbY2"; - this.lbY2.Size = new System.Drawing.Size(20, 20); + this.lbY2.Size = new System.Drawing.Size(14, 13); this.lbY2.TabIndex = 50; this.lbY2.Text = "Y"; // // txtOrientationY // - this.txtOrientationY.Location = new System.Drawing.Point(392, 58); - this.txtOrientationY.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtOrientationY.Location = new System.Drawing.Point(261, 38); this.txtOrientationY.Name = "txtOrientationY"; - this.txtOrientationY.Size = new System.Drawing.Size(55, 26); + this.txtOrientationY.Size = new System.Drawing.Size(38, 20); this.txtOrientationY.TabIndex = 47; this.txtOrientationY.TextChanged += new System.EventHandler(this.txtOrientationY_TextChanged); // // lbId // this.lbId.AutoSize = true; - this.lbId.Location = new System.Drawing.Point(198, 148); - this.lbId.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbId.Location = new System.Drawing.Point(132, 96); this.lbId.Name = "lbId"; - this.lbId.Size = new System.Drawing.Size(78, 20); + this.lbId.Size = new System.Drawing.Size(54, 13); this.lbId.TabIndex = 57; this.lbId.Text = "Marker id:"; // // lbZ2 // this.lbZ2.AutoSize = true; - this.lbZ2.Location = new System.Drawing.Point(476, 29); - this.lbZ2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbZ2.Location = new System.Drawing.Point(317, 19); this.lbZ2.Name = "lbZ2"; - this.lbZ2.Size = new System.Drawing.Size(19, 20); + this.lbZ2.Size = new System.Drawing.Size(14, 13); this.lbZ2.TabIndex = 51; this.lbZ2.Text = "Z"; // // txtOrientationX // - this.txtOrientationX.Location = new System.Drawing.Point(326, 58); - this.txtOrientationX.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtOrientationX.Location = new System.Drawing.Point(217, 38); this.txtOrientationX.Name = "txtOrientationX"; - this.txtOrientationX.Size = new System.Drawing.Size(55, 26); + this.txtOrientationX.Size = new System.Drawing.Size(38, 20); this.txtOrientationX.TabIndex = 46; this.txtOrientationX.TextChanged += new System.EventHandler(this.txtOrientationX_TextChanged); // // btAdd // - this.btAdd.Location = new System.Drawing.Point(202, 195); - this.btAdd.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.btAdd.Location = new System.Drawing.Point(135, 127); this.btAdd.Name = "btAdd"; - this.btAdd.Size = new System.Drawing.Size(138, 35); + this.btAdd.Size = new System.Drawing.Size(92, 23); this.btAdd.TabIndex = 56; this.btAdd.Text = "Add marker"; this.btAdd.UseVisualStyleBackColor = true; @@ -308,58 +325,51 @@ // lbTranslation // this.lbTranslation.AutoSize = true; - this.lbTranslation.Location = new System.Drawing.Point(198, 105); - this.lbTranslation.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbTranslation.Location = new System.Drawing.Point(132, 68); this.lbTranslation.Name = "lbTranslation"; - this.lbTranslation.Size = new System.Drawing.Size(91, 20); + this.lbTranslation.Size = new System.Drawing.Size(62, 13); this.lbTranslation.TabIndex = 52; this.lbTranslation.Text = "Translation:"; // // lbOrientation // this.lbOrientation.AutoSize = true; - this.lbOrientation.Location = new System.Drawing.Point(198, 63); - this.lbOrientation.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbOrientation.Location = new System.Drawing.Point(132, 41); this.lbOrientation.Name = "lbOrientation"; - this.lbOrientation.Size = new System.Drawing.Size(91, 20); + this.lbOrientation.Size = new System.Drawing.Size(61, 13); this.lbOrientation.TabIndex = 45; this.lbOrientation.Text = "Orientation:"; // // txtTranslationZ // - this.txtTranslationZ.Location = new System.Drawing.Point(458, 100); - this.txtTranslationZ.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtTranslationZ.Location = new System.Drawing.Point(305, 65); this.txtTranslationZ.Name = "txtTranslationZ"; - this.txtTranslationZ.Size = new System.Drawing.Size(55, 26); + this.txtTranslationZ.Size = new System.Drawing.Size(38, 20); this.txtTranslationZ.TabIndex = 55; this.txtTranslationZ.TextChanged += new System.EventHandler(this.txtTranslationZ_TextChanged); // // txtTranslationX // - this.txtTranslationX.Location = new System.Drawing.Point(326, 100); - this.txtTranslationX.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtTranslationX.Location = new System.Drawing.Point(217, 65); this.txtTranslationX.Name = "txtTranslationX"; - this.txtTranslationX.Size = new System.Drawing.Size(55, 26); + this.txtTranslationX.Size = new System.Drawing.Size(38, 20); this.txtTranslationX.TabIndex = 53; this.txtTranslationX.TextChanged += new System.EventHandler(this.txtTranslationX_TextChanged); // // lisMarkers // this.lisMarkers.FormattingEnabled = true; - this.lisMarkers.ItemHeight = 20; - this.lisMarkers.Location = new System.Drawing.Point(9, 63); - this.lisMarkers.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.lisMarkers.Location = new System.Drawing.Point(6, 41); this.lisMarkers.Name = "lisMarkers"; - this.lisMarkers.Size = new System.Drawing.Size(178, 164); + this.lisMarkers.Size = new System.Drawing.Size(120, 108); this.lisMarkers.TabIndex = 43; this.lisMarkers.SelectedIndexChanged += new System.EventHandler(this.lisMarkers_SelectedIndexChanged); // // txtTranslationY // - this.txtTranslationY.Location = new System.Drawing.Point(392, 100); - this.txtTranslationY.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtTranslationY.Location = new System.Drawing.Point(261, 65); this.txtTranslationY.Name = "txtTranslationY"; - this.txtTranslationY.Size = new System.Drawing.Size(55, 26); + this.txtTranslationY.Size = new System.Drawing.Size(38, 20); this.txtTranslationY.TabIndex = 54; this.txtTranslationY.TextChanged += new System.EventHandler(this.txtTranslationY_TextChanged); // @@ -376,11 +386,9 @@ this.grBounding.Controls.Add(this.lbZ); this.grBounding.Controls.Add(this.lbY); this.grBounding.Controls.Add(this.lbX); - this.grBounding.Location = new System.Drawing.Point(14, 29); - this.grBounding.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.grBounding.Location = new System.Drawing.Point(9, 19); this.grBounding.Name = "grBounding"; - this.grBounding.Padding = new System.Windows.Forms.Padding(4, 5, 4, 5); - this.grBounding.Size = new System.Drawing.Size(374, 140); + this.grBounding.Size = new System.Drawing.Size(249, 91); this.grBounding.TabIndex = 46; this.grBounding.TabStop = false; this.grBounding.Text = "Bounding box"; @@ -388,104 +396,93 @@ // lbMin // this.lbMin.AutoSize = true; - this.lbMin.Location = new System.Drawing.Point(12, 58); - this.lbMin.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbMin.Location = new System.Drawing.Point(8, 38); this.lbMin.Name = "lbMin"; - this.lbMin.Size = new System.Drawing.Size(95, 20); + this.lbMin.Size = new System.Drawing.Size(65, 13); this.lbMin.TabIndex = 13; this.lbMin.Text = "Min bounds:"; // // txtMaxZ // - this.txtMaxZ.Location = new System.Drawing.Point(272, 95); - this.txtMaxZ.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtMaxZ.Location = new System.Drawing.Point(181, 62); this.txtMaxZ.Name = "txtMaxZ"; - this.txtMaxZ.Size = new System.Drawing.Size(55, 26); + this.txtMaxZ.Size = new System.Drawing.Size(38, 20); this.txtMaxZ.TabIndex = 23; this.txtMaxZ.TextChanged += new System.EventHandler(this.txtMaxZ_TextChanged); // // txtMaxY // - this.txtMaxY.Location = new System.Drawing.Point(206, 95); - this.txtMaxY.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtMaxY.Location = new System.Drawing.Point(137, 62); this.txtMaxY.Name = "txtMaxY"; - this.txtMaxY.Size = new System.Drawing.Size(55, 26); + this.txtMaxY.Size = new System.Drawing.Size(38, 20); this.txtMaxY.TabIndex = 22; this.txtMaxY.TextChanged += new System.EventHandler(this.txtMaxY_TextChanged); // // txtMinX // - this.txtMinX.Location = new System.Drawing.Point(140, 54); - this.txtMinX.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtMinX.Location = new System.Drawing.Point(93, 35); this.txtMinX.Name = "txtMinX"; - this.txtMinX.Size = new System.Drawing.Size(55, 26); + this.txtMinX.Size = new System.Drawing.Size(38, 20); this.txtMinX.TabIndex = 14; this.txtMinX.TextChanged += new System.EventHandler(this.txtMinX_TextChanged); // // txtMaxX // - this.txtMaxX.Location = new System.Drawing.Point(140, 95); - this.txtMaxX.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtMaxX.Location = new System.Drawing.Point(93, 62); this.txtMaxX.Name = "txtMaxX"; - this.txtMaxX.Size = new System.Drawing.Size(55, 26); + this.txtMaxX.Size = new System.Drawing.Size(38, 20); this.txtMaxX.TabIndex = 21; this.txtMaxX.TextChanged += new System.EventHandler(this.txtMaxX_TextChanged); // // txtMinY // - this.txtMinY.Location = new System.Drawing.Point(206, 54); - this.txtMinY.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtMinY.Location = new System.Drawing.Point(137, 35); this.txtMinY.Name = "txtMinY"; - this.txtMinY.Size = new System.Drawing.Size(55, 26); + this.txtMinY.Size = new System.Drawing.Size(38, 20); this.txtMinY.TabIndex = 15; this.txtMinY.TextChanged += new System.EventHandler(this.txtMinY_TextChanged); // // lbMax // this.lbMax.AutoSize = true; - this.lbMax.Location = new System.Drawing.Point(12, 100); - this.lbMax.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbMax.Location = new System.Drawing.Point(8, 65); this.lbMax.Name = "lbMax"; - this.lbMax.Size = new System.Drawing.Size(99, 20); + this.lbMax.Size = new System.Drawing.Size(68, 13); this.lbMax.TabIndex = 20; this.lbMax.Text = "Max bounds:"; // // txtMinZ // - this.txtMinZ.Location = new System.Drawing.Point(272, 54); - this.txtMinZ.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtMinZ.Location = new System.Drawing.Point(181, 35); this.txtMinZ.Name = "txtMinZ"; - this.txtMinZ.Size = new System.Drawing.Size(55, 26); + this.txtMinZ.Size = new System.Drawing.Size(38, 20); this.txtMinZ.TabIndex = 16; this.txtMinZ.TextChanged += new System.EventHandler(this.txtMinZ_TextChanged); // // lbZ // this.lbZ.AutoSize = true; - this.lbZ.Location = new System.Drawing.Point(290, 25); - this.lbZ.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbZ.Location = new System.Drawing.Point(193, 16); this.lbZ.Name = "lbZ"; - this.lbZ.Size = new System.Drawing.Size(19, 20); + this.lbZ.Size = new System.Drawing.Size(14, 13); this.lbZ.TabIndex = 19; this.lbZ.Text = "Z"; // // lbY // this.lbY.AutoSize = true; - this.lbY.Location = new System.Drawing.Point(225, 25); - this.lbY.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbY.Location = new System.Drawing.Point(150, 16); this.lbY.Name = "lbY"; - this.lbY.Size = new System.Drawing.Size(20, 20); + this.lbY.Size = new System.Drawing.Size(14, 13); this.lbY.TabIndex = 18; this.lbY.Text = "Y"; // // lbX // this.lbX.AutoSize = true; - this.lbX.Location = new System.Drawing.Point(159, 25); - this.lbX.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbX.Location = new System.Drawing.Point(106, 16); this.lbX.Name = "lbX"; - this.lbX.Size = new System.Drawing.Size(20, 20); + this.lbX.Size = new System.Drawing.Size(14, 13); this.lbX.TabIndex = 17; this.lbX.Text = "X"; // @@ -496,31 +493,27 @@ this.grFiltering.Controls.Add(this.lbFilterNeighbors); this.grFiltering.Controls.Add(this.lbFilterDistance); this.grFiltering.Controls.Add(this.txtFilterDistance); - this.grFiltering.Location = new System.Drawing.Point(14, 178); - this.grFiltering.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.grFiltering.Location = new System.Drawing.Point(9, 116); this.grFiltering.Name = "grFiltering"; - this.grFiltering.Padding = new System.Windows.Forms.Padding(4, 5, 4, 5); - this.grFiltering.Size = new System.Drawing.Size(374, 135); + this.grFiltering.Size = new System.Drawing.Size(249, 88); this.grFiltering.TabIndex = 45; this.grFiltering.TabStop = false; this.grFiltering.Text = "Filtering"; // // txtFilterNeighbors // - this.txtFilterNeighbors.Location = new System.Drawing.Point(140, 60); - this.txtFilterNeighbors.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtFilterNeighbors.Location = new System.Drawing.Point(93, 39); this.txtFilterNeighbors.Name = "txtFilterNeighbors"; - this.txtFilterNeighbors.Size = new System.Drawing.Size(55, 26); + this.txtFilterNeighbors.Size = new System.Drawing.Size(38, 20); this.txtFilterNeighbors.TabIndex = 20; this.txtFilterNeighbors.TextChanged += new System.EventHandler(this.txtFilterNeighbors_TextChanged); // // chFilter // this.chFilter.AutoSize = true; - this.chFilter.Location = new System.Drawing.Point(140, 25); - this.chFilter.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.chFilter.Location = new System.Drawing.Point(93, 16); this.chFilter.Name = "chFilter"; - this.chFilter.Size = new System.Drawing.Size(147, 24); + this.chFilter.Size = new System.Drawing.Size(100, 17); this.chFilter.TabIndex = 18; this.chFilter.Text = "filtering enabled"; this.chFilter.UseVisualStyleBackColor = true; @@ -529,29 +522,26 @@ // lbFilterNeighbors // this.lbFilterNeighbors.AutoSize = true; - this.lbFilterNeighbors.Location = new System.Drawing.Point(12, 65); - this.lbFilterNeighbors.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbFilterNeighbors.Location = new System.Drawing.Point(8, 42); this.lbFilterNeighbors.Name = "lbFilterNeighbors"; - this.lbFilterNeighbors.Size = new System.Drawing.Size(98, 20); + this.lbFilterNeighbors.Size = new System.Drawing.Size(67, 13); this.lbFilterNeighbors.TabIndex = 19; this.lbFilterNeighbors.Text = "N neighbors:"; // // lbFilterDistance // this.lbFilterDistance.AutoSize = true; - this.lbFilterDistance.Location = new System.Drawing.Point(12, 105); - this.lbFilterDistance.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lbFilterDistance.Location = new System.Drawing.Point(8, 68); this.lbFilterDistance.Name = "lbFilterDistance"; - this.lbFilterDistance.Size = new System.Drawing.Size(106, 20); + this.lbFilterDistance.Size = new System.Drawing.Size(73, 13); this.lbFilterDistance.TabIndex = 21; this.lbFilterDistance.Text = "Max distance:"; // // txtFilterDistance // - this.txtFilterDistance.Location = new System.Drawing.Point(140, 100); - this.txtFilterDistance.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtFilterDistance.Location = new System.Drawing.Point(93, 65); this.txtFilterDistance.Name = "txtFilterDistance"; - this.txtFilterDistance.Size = new System.Drawing.Size(55, 26); + this.txtFilterDistance.Size = new System.Drawing.Size(38, 20); this.txtFilterDistance.TabIndex = 22; this.txtFilterDistance.TextChanged += new System.EventHandler(this.txtFilterDistance_TextChanged); // @@ -568,79 +558,13 @@ this.grServer.Controls.Add(this.txtICPIters); this.grServer.Controls.Add(this.chMerge); this.grServer.Controls.Add(this.lbICPIters); - this.grServer.Location = new System.Drawing.Point(18, 478); - this.grServer.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.grServer.Location = new System.Drawing.Point(12, 311); this.grServer.Name = "grServer"; - this.grServer.Padding = new System.Windows.Forms.Padding(4, 5, 4, 5); - this.grServer.Size = new System.Drawing.Size(948, 175); + this.grServer.Size = new System.Drawing.Size(632, 114); this.grServer.TabIndex = 44; this.grServer.TabStop = false; this.grServer.Text = "KinectServer settings"; // - // rBinaryPly - // - this.rBinaryPly.AutoSize = true; - this.rBinaryPly.Location = new System.Drawing.Point(277, 105); - this.rBinaryPly.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); - this.rBinaryPly.Name = "rBinaryPly"; - this.rBinaryPly.Size = new System.Drawing.Size(110, 24); - this.rBinaryPly.TabIndex = 30; - this.rBinaryPly.TabStop = true; - this.rBinaryPly.Text = "binary PLY"; - this.rBinaryPly.UseVisualStyleBackColor = true; - this.rBinaryPly.CheckedChanged += new System.EventHandler(this.PlyFormat_CheckedChanged); - // - // lbFormat - // - this.lbFormat.AutoSize = true; - this.lbFormat.Location = new System.Drawing.Point(7, 106); - this.lbFormat.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.lbFormat.Name = "lbFormat"; - this.lbFormat.Size = new System.Drawing.Size(88, 20); - this.lbFormat.TabIndex = 29; - this.lbFormat.Text = "File format:"; - // - // rAsciiPly - // - this.rAsciiPly.AutoSize = true; - this.rAsciiPly.Location = new System.Drawing.Point(156, 105); - this.rAsciiPly.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); - this.rAsciiPly.Name = "rAsciiPly"; - this.rAsciiPly.Size = new System.Drawing.Size(111, 24); - this.rAsciiPly.TabIndex = 28; - this.rAsciiPly.TabStop = true; - this.rAsciiPly.Text = "ASCII PLY"; - this.rAsciiPly.UseVisualStyleBackColor = true; - this.rAsciiPly.CheckedChanged += new System.EventHandler(this.PlyFormat_CheckedChanged); - // - // txtRefinIters - // - this.txtRefinIters.Location = new System.Drawing.Point(408, 31); - this.txtRefinIters.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); - this.txtRefinIters.Name = "txtRefinIters"; - this.txtRefinIters.Size = new System.Drawing.Size(55, 26); - this.txtRefinIters.TabIndex = 27; - this.txtRefinIters.TextChanged += new System.EventHandler(this.txtRefinIters_TextChanged); - // - // lbOuterIters - // - this.lbOuterIters.AutoSize = true; - this.lbOuterIters.Location = new System.Drawing.Point(222, 36); - this.lbOuterIters.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.lbOuterIters.Name = "lbOuterIters"; - this.lbOuterIters.Size = new System.Drawing.Size(178, 20); - this.lbOuterIters.TabIndex = 26; - this.lbOuterIters.Text = "Num of refinement iters:"; - // - // label2 - // - this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(8, 139); - this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(141, 20); - this.label2.TabIndex = 31; - this.label2.Text = "Compression level:"; - // // cbCompressionLevel // this.cbCompressionLevel.FormattingEnabled = true; @@ -657,26 +581,88 @@ "15", "17", "19"}); - this.cbCompressionLevel.Location = new System.Drawing.Point(154, 136); + this.cbCompressionLevel.Location = new System.Drawing.Point(103, 88); + this.cbCompressionLevel.Margin = new System.Windows.Forms.Padding(2); this.cbCompressionLevel.Name = "cbCompressionLevel"; - this.cbCompressionLevel.Size = new System.Drawing.Size(169, 28); + this.cbCompressionLevel.Size = new System.Drawing.Size(114, 21); this.cbCompressionLevel.TabIndex = 32; this.cbCompressionLevel.SelectedIndexChanged += new System.EventHandler(this.cbCompressionLevel_SelectedIndexChanged); // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(5, 90); + this.label2.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(95, 13); + this.label2.TabIndex = 31; + this.label2.Text = "Compression level:"; + // + // rBinaryPly + // + this.rBinaryPly.AutoSize = true; + this.rBinaryPly.Location = new System.Drawing.Point(185, 68); + this.rBinaryPly.Name = "rBinaryPly"; + this.rBinaryPly.Size = new System.Drawing.Size(76, 17); + this.rBinaryPly.TabIndex = 30; + this.rBinaryPly.TabStop = true; + this.rBinaryPly.Text = "binary PLY"; + this.rBinaryPly.UseVisualStyleBackColor = true; + this.rBinaryPly.CheckedChanged += new System.EventHandler(this.PlyFormat_CheckedChanged); + // + // lbFormat + // + this.lbFormat.AutoSize = true; + this.lbFormat.Location = new System.Drawing.Point(5, 69); + this.lbFormat.Name = "lbFormat"; + this.lbFormat.Size = new System.Drawing.Size(58, 13); + this.lbFormat.TabIndex = 29; + this.lbFormat.Text = "File format:"; + // + // rAsciiPly + // + this.rAsciiPly.AutoSize = true; + this.rAsciiPly.Location = new System.Drawing.Point(104, 68); + this.rAsciiPly.Name = "rAsciiPly"; + this.rAsciiPly.Size = new System.Drawing.Size(75, 17); + this.rAsciiPly.TabIndex = 28; + this.rAsciiPly.TabStop = true; + this.rAsciiPly.Text = "ASCII PLY"; + this.rAsciiPly.UseVisualStyleBackColor = true; + this.rAsciiPly.CheckedChanged += new System.EventHandler(this.PlyFormat_CheckedChanged); + // + // txtRefinIters + // + this.txtRefinIters.Location = new System.Drawing.Point(272, 20); + this.txtRefinIters.Name = "txtRefinIters"; + this.txtRefinIters.Size = new System.Drawing.Size(38, 20); + this.txtRefinIters.TabIndex = 27; + this.txtRefinIters.TextChanged += new System.EventHandler(this.txtRefinIters_TextChanged); + // + // lbOuterIters + // + this.lbOuterIters.AutoSize = true; + this.lbOuterIters.Location = new System.Drawing.Point(148, 23); + this.lbOuterIters.Name = "lbOuterIters"; + this.lbOuterIters.Size = new System.Drawing.Size(118, 13); + this.lbOuterIters.TabIndex = 26; + this.lbOuterIters.Text = "Num of refinement iters:"; + // // SettingsForm // - this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(984, 663); + this.ClientSize = new System.Drawing.Size(656, 431); this.Controls.Add(this.grServer); this.Controls.Add(this.grClient); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; - this.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.MaximizeBox = false; this.Name = "SettingsForm"; this.Text = "Settings"; this.Load += new System.EventHandler(this.SettingsForm_Load); this.grClient.ResumeLayout(false); + this.grSync.ResumeLayout(false); + this.grSync.PerformLayout(); this.grBody.ResumeLayout(false); this.grBody.PerformLayout(); this.grMarkers.ResumeLayout(false); @@ -744,6 +730,7 @@ private System.Windows.Forms.CheckBox chSkeletons; private System.Windows.Forms.ComboBox cbCompressionLevel; private System.Windows.Forms.Label label2; - + private System.Windows.Forms.GroupBox grSync; + private System.Windows.Forms.CheckBox chSync; } } \ No newline at end of file diff --git a/LiveScanServer/SettingsForm.cs b/LiveScanServer/SettingsForm.cs index 8bae0b1..ddb9b65 100644 --- a/LiveScanServer/SettingsForm.cs +++ b/LiveScanServer/SettingsForm.cs @@ -61,6 +61,9 @@ namespace KinectServer chMerge.Checked = oSettings.bMergeScansForSave; txtICPIters.Text = oSettings.nNumICPIterations.ToString(); txtRefinIters.Text = oSettings.nNumRefineIters.ToString(); + + chSync.Checked = oSettings.bEnableSync; + if (oSettings.bSaveAsBinaryPLY) { rBinaryPly.Checked = true; @@ -78,7 +81,11 @@ namespace KinectServer void UpdateClients() { if (bFormLoaded) + { oServer.SendSettings(); + oServer.SendTemporalSyncData(); + } + } void UpdateMarkerFields() @@ -340,5 +347,14 @@ namespace KinectServer } UpdateClients(); } + + + private void chSync_CheckedChanged(object sender, EventArgs e) + { + oSettings.bEnableSync = chSync.Checked; + UpdateClients(); + } + + } } diff --git a/LiveScanServer/SettingsForm.resx b/LiveScanServer/SettingsForm.resx index 1af7de1..7c1ce7f 100644 --- a/LiveScanServer/SettingsForm.resx +++ b/LiveScanServer/SettingsForm.resx @@ -117,4 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + False + \ No newline at end of file diff --git a/include/LiveScanClient/liveScanClient.h b/include/LiveScanClient/liveScanClient.h index 0fcc273..cf4b33d 100644 --- a/include/LiveScanClient/liveScanClient.h +++ b/include/LiveScanClient/liveScanClient.h @@ -43,6 +43,9 @@ private: bool m_bFilter; bool m_bStreamOnlyBodies; + bool m_bIsMaster; + bool m_bIsSubOrdinate; + ICapture *pCapture; int m_nFilterNeighbors; @@ -51,6 +54,8 @@ private: bool m_bCaptureFrame; bool m_bConnected; bool m_bConfirmCaptured; + bool m_bConfirmMaster; + bool m_bConfirmSubOrdinate; bool m_bConfirmCalibrated; bool m_bShowDepth; bool m_bFrameCompression; diff --git a/include/LiveScanClient/utils.h b/include/LiveScanClient/utils.h index ce85698..8dcb5ca 100644 --- a/include/LiveScanClient/utils.h +++ b/include/LiveScanClient/utils.h @@ -27,7 +27,9 @@ enum INCOMING_MESSAGE_TYPE MSG_REQUEST_STORED_FRAME, MSG_REQUEST_LAST_FRAME, MSG_RECEIVE_CALIBRATION, - MSG_CLEAR_STORED_FRAMES + MSG_CLEAR_STORED_FRAMES, + MSG_SET_MASTER, + MSG_SET_SUBORDINATE }; enum OUTGOING_MESSAGE_TYPE @@ -35,7 +37,8 @@ enum OUTGOING_MESSAGE_TYPE MSG_CONFIRM_CAPTURED, MSG_CONFIRM_CALIBRATED, MSG_STORED_FRAME, - MSG_LAST_FRAME + MSG_LAST_FRAME, + MSG_CONFIRM_TEMP_SYNC_STATUS }; typedef struct Point3f diff --git a/src/LiveScanClient/liveScanClient.cpp b/src/LiveScanClient/liveScanClient.cpp index 5ea01f7..f3d69a1 100644 --- a/src/LiveScanClient/liveScanClient.cpp +++ b/src/LiveScanClient/liveScanClient.cpp @@ -444,6 +444,16 @@ void LiveScanClient::HandleSocket() //calibrate else if (received[i] == MSG_CALIBRATE) m_bCalibrate = true; + //Set this client as master + else if (received[i] == MSG_SET_MASTER) { + m_bIsMaster = true; + m_bIsSubOrdinate = false; + } + //Set this client as subordinate + else if (received[i] == MSG_SET_SUBORDINATE) { + m_bIsMaster = false; + m_bIsSubOrdinate = true; + } //receive settings //TODO: what if packet is split? else if (received[i] == MSG_RECEIVE_SETTINGS) @@ -567,6 +577,24 @@ void LiveScanClient::HandleSocket() m_bConfirmCaptured = false; } + if (m_bConfirmMaster) + { + int size = 2; + char *buffer = new char[size]; + buffer[0] = MSG_CONFIRM_TEMP_SYNC_STATUS; + buffer[1] = 0; + m_pClientSocket->SendBytes(buffer, size); + } + + if (m_bConfirmSubOrdinate) + { + int size = 2; + char* buffer = new char[size]; + buffer[0] = MSG_CONFIRM_TEMP_SYNC_STATUS; + buffer[1] = -1; + m_pClientSocket->SendBytes(buffer, size); + } + if (m_bConfirmCalibrated) { int size = (9 + 3) * sizeof(float) + sizeof(int) + 1; From 1544abd2763b0df04aed39666f2bdc1a5e94978d Mon Sep 17 00:00:00 2001 From: Christopher Remde Date: Wed, 8 Jul 2020 13:55:48 +0200 Subject: [PATCH 2/6] Finished Temporal Sync Feature --- ICP/ICP.vcxproj | 14 +-- LiveScanClient/KinectClient.vcxproj | 14 +-- LiveScanClient/packages.config | 2 +- LiveScanServer/KinectServer.cs | 65 +++++++++- LiveScanServer/KinectSocket.cs | 103 ++++++++++----- LiveScanServer/MainWindowForm.cs | 3 + LiveScanServer/SettingsForm.Designer.cs | 42 ++++--- LiveScanServer/SettingsForm.cs | 10 +- LiveScanServer/SettingsForm.resx | 3 - include/LiveScanClient/azureKinectCapture.h | 11 +- .../LiveScanClient/frameFileWriterReader.h | 2 +- include/LiveScanClient/iCapture.h | 7 +- include/LiveScanClient/liveScanClient.h | 8 +- include/LiveScanClient/utils.h | 5 +- src/LiveScanClient/azureKinectCapture.cpp | 118 +++++++++++++++--- src/LiveScanClient/frameFileWriterReader.cpp | 5 +- src/LiveScanClient/liveScanClient.cpp | 96 ++++++++++---- 17 files changed, 390 insertions(+), 118 deletions(-) diff --git a/ICP/ICP.vcxproj b/ICP/ICP.vcxproj index 227df44..18f8595 100644 --- a/ICP/ICP.vcxproj +++ b/ICP/ICP.vcxproj @@ -29,46 +29,46 @@ {973EE923-B423-4BCD-AA08-B03DA40CB51F} ICP - 10.0.17763.0 + 10.0 Application true - v141 + v142 MultiByte Application true - v141 + v142 MultiByte Application false - v141 + v142 true MultiByte Application false - v141 + v142 true MultiByte DynamicLibrary false - v141 + v142 true MultiByte DynamicLibrary false - v141 + v142 true MultiByte diff --git a/LiveScanClient/KinectClient.vcxproj b/LiveScanClient/KinectClient.vcxproj index 9900606..eaf094b 100644 --- a/LiveScanClient/KinectClient.vcxproj +++ b/LiveScanClient/KinectClient.vcxproj @@ -60,32 +60,32 @@ {9B550BBA-EAFB-4D12-8B1C-8FDA39361F52} KinectClient LiveScanClient - 10.0.17763.0 + 10.0 Application true - v141 + v142 Unicode Application true - v141 + v142 Unicode Application false - v141 + v142 true Unicode Application false - v141 + v142 true Unicode @@ -193,12 +193,12 @@ - + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/LiveScanClient/packages.config b/LiveScanClient/packages.config index 49caf13..bd5be72 100644 --- a/LiveScanClient/packages.config +++ b/LiveScanClient/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/LiveScanServer/KinectServer.cs b/LiveScanServer/KinectServer.cs index 4f9045c..1d7d69c 100644 --- a/LiveScanServer/KinectServer.cs +++ b/LiveScanServer/KinectServer.cs @@ -31,6 +31,7 @@ namespace KinectServer Socket oServerSocket; bool bServerRunning = false; + bool waitForSubToStart = false; KinectSettings oSettings; object oClientSocketLock = new object(); @@ -232,19 +233,71 @@ namespace KinectServer } } + /// + /// Send temporal Sync Data to all client + /// public void SendTemporalSyncData() { lock (oClientSocketLock) - { - lClientSockets[0].SendTemporalSyncStatus(true, false); - - for (int i = 1; i < lClientSockets.Count; i++) + { + if(lClientSockets.Count > 1 && oSettings.bEnableSync) { - lClientSockets[i].SendTemporalSyncStatus(false, true); + byte syncOffSetCounter = 1; + + for (int i = 0; i < lClientSockets.Count; i++) + { + lClientSockets[i].SendTemporalSyncStatus(true, syncOffSetCounter); + syncOffSetCounter++; + } + + waitForSubToStart = true; + } + + else + { + for (int i = 0; i < lClientSockets.Count; i++) + { + lClientSockets[i].SendTemporalSyncStatus(false, 0); + } } } } + /// + /// Called when a sub client has started. This checks if all sub clients have already started. If yes, we initialize the master + /// + public void CheckForMasterStart() + { + if (!oSettings.bEnableSync || !waitForSubToStart) + { + return; + } + + bool allSubsStarted = true; + + lock (oClientSocketLock) + { + for (int i = 0; i < lClientSockets.Count; i++) + { + if (!lClientSockets[i].bSubStarted && lClientSockets[i].currentTempSyncState == KinectSocket.eTempSyncConfig.SUBORDINATE) + { + allSubsStarted = false; + break; + } + } + + if (allSubsStarted) + { + for (int i = 0; i < lClientSockets.Count; i++) + { + lClientSockets[i].SendMasterInitialize(); + } + } + } + + + } + public bool GetStoredFrame(List> lFramesRGB, List> lFramesVerts) { bool bNoMoreStoredFrames; @@ -373,6 +426,8 @@ namespace KinectServer lClientSockets.Add(new KinectSocket(newClient)); lClientSockets[lClientSockets.Count - 1].SendSettings(oSettings); lClientSockets[lClientSockets.Count - 1].eChanged += new SocketChangedHandler(SocketListChanged); + lClientSockets[lClientSockets.Count - 1].eSubInitialized += new SubOrdinateInitialized(CheckForMasterStart); + if (eSocketListChanged != null) { eSocketListChanged(lClientSockets); diff --git a/LiveScanServer/KinectSocket.cs b/LiveScanServer/KinectSocket.cs index 065a9a0..7d1f9b1 100644 --- a/LiveScanServer/KinectSocket.cs +++ b/LiveScanServer/KinectSocket.cs @@ -23,6 +23,7 @@ using System.Net.Sockets; namespace KinectServer { public delegate void SocketChangedHandler(); + public delegate void SubOrdinateInitialized(); public class KinectSocket { @@ -33,8 +34,10 @@ namespace KinectServer public bool bStoredFrameReceived = false; public bool bNoMoreStoredFrames = true; public bool bCalibrated = false; - public bool bIsMaster = false; - public bool bIsSubOrdinate = false; + public bool bSubStarted = false; + + public enum eTempSyncConfig { MASTER, SUBORDINATE, STANDALONE } + public eTempSyncConfig currentTempSyncState = eTempSyncConfig.STANDALONE; //The pose of the sensor in the scene (used by the OpenGLWindow to show the sensor) public AffineTransform oCameraPose = new AffineTransform(); //The transform that maps the vertices in the sensor coordinate system to the world corrdinate system. @@ -47,11 +50,14 @@ namespace KinectServer public List lBodies = new List(); public event SocketChangedHandler eChanged; + public event SubOrdinateInitialized eSubInitialized; public KinectSocket(Socket clientSocket) { oSocket = clientSocket; sSocketState = oSocket.RemoteEndPoint.ToString() + " Calibrated = false"; + + UpdateSocketState(); } public void CaptureFrame() @@ -123,23 +129,38 @@ namespace KinectServer SendByte(); } - public void SendTemporalSyncStatus(bool isMaster, bool isSubOrdinate) - { - if (isMaster) + /// + /// Send the device the command to restart with Temporal Sync Enabled or Disabled + /// + /// Enable/Disable Temporal Sync + /// Only set when this device should be a subordinate. Should be a unique number in ascending order for each sub + public void SendTemporalSyncStatus(bool tempSyncOn, byte syncOffSetMultiplier) + { + if (tempSyncOn) { - byteToSend[0] = 7; - SendByte(); - return; + byte[] data = new byte[2]; + data[0] = 7; + data[1] = syncOffSetMultiplier; + if (SocketConnected()) + oSocket.Send(data); } - if (isSubOrdinate) + else { byteToSend[0] = 8; SendByte(); - return; } } + /// + /// Sends the master device the command to Start. Should only be called when all subs have started + /// + public void SendMasterInitialize() + { + byteToSend[0] = 9; + SendByte(); + } + public void ReceiveCalibrationData() { bCalibrated = true; @@ -167,9 +188,10 @@ namespace KinectServer UpdateSocketState(); } + //Gets the TemporalSyncStatus from the device public void ReceiveTemporalSyncStatus() { - int nToRead; + byte tempSyncState; byte[] buffer = new byte[1024]; while (oSocket.Available == 0) @@ -178,24 +200,40 @@ namespace KinectServer return; } - oSocket.Receive(buffer, 4, SocketFlags.None); - nToRead = BitConverter.ToInt32(buffer, 0); + oSocket.Receive(buffer, 3, SocketFlags.None); + tempSyncState = buffer[0]; - if (nToRead == -1) + if (tempSyncState == 0) { - bIsSubOrdinate = true; - bIsMaster = false; - return; + currentTempSyncState = eTempSyncConfig.SUBORDINATE; + bSubStarted = true; + eSubInitialized?.Invoke(); } - if(nToRead == 0) + else if(tempSyncState == 1) { - bIsMaster = true; - bIsSubOrdinate = false; - return; + currentTempSyncState = eTempSyncConfig.MASTER; } + else if (tempSyncState == 2) + { + currentTempSyncState = eTempSyncConfig.STANDALONE; + } + + //MARKED FOR DELETION + //if(currentTempSyncState == eTempSyncConfig.SUBORDINATE && confirmSubStarted == 1) + //{ + + //} + + //else if(currentTempSyncState == eTempSyncConfig.SUBORDINATE && confirmSubStarted == 0) + //{ + // bSubStarted = false; + // eSubInitialized?.Invoke(); + //} + UpdateSocketState(); + } public void ReceiveFrame() @@ -343,22 +381,23 @@ namespace KinectServer public void UpdateSocketState() { - string tempSyncStatus = ""; + string tempSyncMessage = ""; - if (bIsMaster) + switch (currentTempSyncState) { - tempSyncStatus = "[Master]"; + case eTempSyncConfig.MASTER: + tempSyncMessage = "[MASTER]"; + break; + case eTempSyncConfig.SUBORDINATE: + tempSyncMessage = "[SUBORDINATE]"; + break; + default: + break; } - if (bIsSubOrdinate) - { - tempSyncStatus = "[Subordinate]"; - } + sSocketState = oSocket.RemoteEndPoint.ToString() + " Calibrated = " + bCalibrated + " " + tempSyncMessage; - sSocketState = oSocket.RemoteEndPoint.ToString() + " Calibrated = " + bCalibrated + " " + tempSyncStatus; - - if (eChanged != null) - eChanged(); + eChanged?.Invoke(); } } } diff --git a/LiveScanServer/MainWindowForm.cs b/LiveScanServer/MainWindowForm.cs index 93eaff0..7b7fdc2 100644 --- a/LiveScanServer/MainWindowForm.cs +++ b/LiveScanServer/MainWindowForm.cs @@ -75,6 +75,8 @@ namespace KinectServer IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); Stream stream = new FileStream("settings.bin", FileMode.Open, FileAccess.Read); oSettings = (KinectSettings)formatter.Deserialize(stream); + //This always needs to be set to false on startup, as we can only activate it when all Kinects are already connected + oSettings.bEnableSync = false; stream.Close(); } catch(Exception) @@ -496,6 +498,7 @@ namespace KinectServer for (int i = 0; i < socketList.Count; i++) listBoxItems.Add(socketList[i].sSocketState); + lClientListBox.DataSource = listBoxItems; } } diff --git a/LiveScanServer/SettingsForm.Designer.cs b/LiveScanServer/SettingsForm.Designer.cs index c8b4a6c..0387c58 100644 --- a/LiveScanServer/SettingsForm.Designer.cs +++ b/LiveScanServer/SettingsForm.Designer.cs @@ -28,13 +28,13 @@ /// private void InitializeComponent() { - System.Windows.Forms.Label lbSyncWarning; this.lbMerge = new System.Windows.Forms.Label(); this.chMerge = new System.Windows.Forms.CheckBox(); this.lbICPIters = new System.Windows.Forms.Label(); this.txtICPIters = new System.Windows.Forms.TextBox(); this.grClient = new System.Windows.Forms.GroupBox(); this.grSync = new System.Windows.Forms.GroupBox(); + this.label1 = new System.Windows.Forms.Label(); this.chSync = new System.Windows.Forms.CheckBox(); this.grBody = new System.Windows.Forms.GroupBox(); this.chSkeletons = new System.Windows.Forms.CheckBox(); @@ -82,7 +82,7 @@ this.rAsciiPly = new System.Windows.Forms.RadioButton(); this.txtRefinIters = new System.Windows.Forms.TextBox(); this.lbOuterIters = new System.Windows.Forms.Label(); - lbSyncWarning = new System.Windows.Forms.Label(); + this.label3 = new System.Windows.Forms.Label(); this.grClient.SuspendLayout(); this.grSync.SuspendLayout(); this.grBody.SuspendLayout(); @@ -145,24 +145,27 @@ // // grSync // - this.grSync.Controls.Add(lbSyncWarning); + this.grSync.Controls.Add(this.label3); + this.grSync.Controls.Add(this.label1); this.grSync.Controls.Add(this.chSync); this.grSync.Location = new System.Drawing.Point(271, 211); this.grSync.Name = "grSync"; - this.grSync.Size = new System.Drawing.Size(355, 66); + this.grSync.Size = new System.Drawing.Size(355, 80); this.grSync.TabIndex = 48; this.grSync.TabStop = false; - this.grSync.Text = "Temporal Sync (Azure Kinect Only)"; + this.grSync.Text = "Temporal Sync"; + this.grSync.Enter += new System.EventHandler(this.grSync_Enter); // - // lbSyncWarning + // label1 // - lbSyncWarning.AutoSize = true; - lbSyncWarning.CausesValidation = false; - lbSyncWarning.Location = new System.Drawing.Point(3, 41); - lbSyncWarning.Name = "lbSyncWarning"; - lbSyncWarning.Size = new System.Drawing.Size(347, 13); - lbSyncWarning.TabIndex = 1; - lbSyncWarning.Text = "Notice: Devices need to be connected via sync cabel before enabeling!\r\n"; + this.label1.Location = new System.Drawing.Point(7, 41); + this.label1.MaximumSize = new System.Drawing.Size(0, 60); + this.label1.MinimumSize = new System.Drawing.Size(0, 30); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(0, 30); + this.label1.TabIndex = 1; + this.label1.Text = "Notice: All devices need to be connected to the server and via sync cabel before" + + " enabeling! First Device in Server List will become the master"; // // chSync // @@ -174,7 +177,6 @@ this.chSync.Text = "Enable Temporal Sync"; this.chSync.UseVisualStyleBackColor = true; this.chSync.CheckedChanged += new System.EventHandler(this.chSync_CheckedChanged); - // // grBody // @@ -647,6 +649,16 @@ this.lbOuterIters.Size = new System.Drawing.Size(118, 13); this.lbOuterIters.TabIndex = 26; this.lbOuterIters.Text = "Num of refinement iters:"; + // + // label3 + // + this.label3.Location = new System.Drawing.Point(7, 36); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(342, 44); + this.label3.TabIndex = 2; + this.label3.Text = "Before activating, make sure that all Kinects are connected to the server and pro" + + "perly connected via the sync cables. The first device in the server list will be" + + "come the master!"; // // SettingsForm // @@ -732,5 +744,7 @@ private System.Windows.Forms.Label label2; private System.Windows.Forms.GroupBox grSync; private System.Windows.Forms.CheckBox chSync; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label3; } } \ No newline at end of file diff --git a/LiveScanServer/SettingsForm.cs b/LiveScanServer/SettingsForm.cs index ddb9b65..b7a0665 100644 --- a/LiveScanServer/SettingsForm.cs +++ b/LiveScanServer/SettingsForm.cs @@ -355,6 +355,14 @@ namespace KinectServer UpdateClients(); } - + private void grSync_Enter(object sender, EventArgs e) + { + + } + + private void lbSyncWarning_Click(object sender, EventArgs e) + { + + } } } diff --git a/LiveScanServer/SettingsForm.resx b/LiveScanServer/SettingsForm.resx index 7c1ce7f..1af7de1 100644 --- a/LiveScanServer/SettingsForm.resx +++ b/LiveScanServer/SettingsForm.resx @@ -117,7 +117,4 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - False - \ No newline at end of file diff --git a/include/LiveScanClient/azureKinectCapture.h b/include/LiveScanClient/azureKinectCapture.h index facd9b0..b1ccda3 100644 --- a/include/LiveScanClient/azureKinectCapture.h +++ b/include/LiveScanClient/azureKinectCapture.h @@ -11,12 +11,17 @@ public: AzureKinectCapture(); ~AzureKinectCapture(); - bool Initialize(); + bool Initialize(bool asMaster, bool asSubOrdinate, int syncOffset); + bool Close(); bool AcquireFrame(); void MapDepthFrameToCameraSpace(Point3f *pCameraSpacePoints); void MapColorFrameToCameraSpace(Point3f *pCameraSpacePoints); void MapDepthFrameToColorSpace(UINT16 *pDepthInColorSpace); void MapColorFrameToDepthSpace(RGB *pColorInDepthSpace); + int GetSyncJackState(); + uint64_t GetTimeStamp(); + + private: k4a_device_t kinectSensor = NULL; int32_t captureTimeoutMs = 1000; @@ -27,6 +32,8 @@ private: k4a_image_t colorImageInDepth = NULL; k4a_image_t depthImageInColor = NULL; k4a_transformation_t transformation = NULL; - + bool syncInConnected = false; + bool syncOutConnected = false; + uint64_t currentTimeStamp; void UpdateDepthPointCloud(); }; \ No newline at end of file diff --git a/include/LiveScanClient/frameFileWriterReader.h b/include/LiveScanClient/frameFileWriterReader.h index cb212c5..5f80668 100644 --- a/include/LiveScanClient/frameFileWriterReader.h +++ b/include/LiveScanClient/frameFileWriterReader.h @@ -16,7 +16,7 @@ public: // leave filename blank if you want the filename to be generated from the date void setCurrentFilename(std::string filename = ""); - void writeFrame(std::vector points, std::vector colors); + void writeFrame(std::vector points, std::vector colors, uint64_t timestamp); bool readFrame(std::vector &outPoints, std::vector &outColors); bool openedForWriting() { return m_bFileOpenedForWriting; } diff --git a/include/LiveScanClient/iCapture.h b/include/LiveScanClient/iCapture.h index 6a85fdb..a0bf2e8 100644 --- a/include/LiveScanClient/iCapture.h +++ b/include/LiveScanClient/iCapture.h @@ -40,12 +40,15 @@ public: ICapture(); ~ICapture(); - virtual bool Initialize() = 0; + virtual bool Initialize(bool asMaster, bool asSubOrdinate, int syncOffset) = 0; virtual bool AcquireFrame() = 0; + virtual bool Close() = 0; virtual void MapDepthFrameToCameraSpace(Point3f *pCameraSpacePoints) = 0; virtual void MapColorFrameToCameraSpace(Point3f *pCameraSpacePoints) = 0; virtual void MapDepthFrameToColorSpace(UINT16 *pColorSpacePoints) = 0; virtual void MapColorFrameToDepthSpace(RGB *pDepthSpacePoints) = 0; + virtual int GetSyncJackState() = 0; + virtual uint64_t GetTimeStamp() = 0; bool bInitialized; @@ -57,5 +60,7 @@ public: RGB *pColorRGBX; std::vector vBodies; + + std::string serialNumber; }; \ No newline at end of file diff --git a/include/LiveScanClient/liveScanClient.h b/include/LiveScanClient/liveScanClient.h index cf4b33d..41f550f 100644 --- a/include/LiveScanClient/liveScanClient.h +++ b/include/LiveScanClient/liveScanClient.h @@ -54,13 +54,16 @@ private: bool m_bCaptureFrame; bool m_bConnected; bool m_bConfirmCaptured; - bool m_bConfirmMaster; - bool m_bConfirmSubOrdinate; + bool m_bConfirmTempSyncState; + bool m_bConfirmSubOrdinateStarted; bool m_bConfirmCalibrated; bool m_bShowDepth; bool m_bFrameCompression; int m_iCompressionLevel; + enum tempSyncConfig { MASTER, SUBORDINATE, STANDALONE }; + tempSyncConfig currentTempSyncState; + FrameFileWriterReader m_framesFileWriterReader; @@ -76,6 +79,7 @@ private: double m_fFreq; INT64 m_nNextStatusTime; DWORD m_nFramesSinceUpdate; + int frameRecordCounter; Point3f* m_pCameraSpaceCoordinates; RGB* m_pColorInDepthSpace; diff --git a/include/LiveScanClient/utils.h b/include/LiveScanClient/utils.h index 8dcb5ca..a6eeed9 100644 --- a/include/LiveScanClient/utils.h +++ b/include/LiveScanClient/utils.h @@ -28,8 +28,9 @@ enum INCOMING_MESSAGE_TYPE MSG_REQUEST_LAST_FRAME, MSG_RECEIVE_CALIBRATION, MSG_CLEAR_STORED_FRAMES, - MSG_SET_MASTER, - MSG_SET_SUBORDINATE + MSG_SET_TEMPSYNC_ON, + MSG_SET_TEMPSYNC_OFF, + MSG_START_MASTER }; enum OUTGOING_MESSAGE_TYPE diff --git a/src/LiveScanClient/azureKinectCapture.cpp b/src/LiveScanClient/azureKinectCapture.cpp index 33d9140..06798fe 100644 --- a/src/LiveScanClient/azureKinectCapture.cpp +++ b/src/LiveScanClient/azureKinectCapture.cpp @@ -17,7 +17,14 @@ AzureKinectCapture::~AzureKinectCapture() k4a_device_close(kinectSensor); } -bool AzureKinectCapture::Initialize() +/// +/// Initialize the device. If you want to initialize it as Standalone, set "asMaster" and "asSubordinate" to false +/// +/// Initializes this device as master +/// Initalizes this devices as subordinate +/// Should only be set when initializing as Subordinate. Each subordinate should have a unique, ascending value +/// +bool AzureKinectCapture::Initialize(bool asMaster, bool asSubordinate, int syncOffsetMultiplier) { uint32_t count = k4a_device_get_installed_count(); int deviceIdx = 0; @@ -40,6 +47,27 @@ bool AzureKinectCapture::Initialize() config.depth_mode = K4A_DEPTH_MODE_NFOV_UNBINNED; config.synchronized_images_only = true; + if (asMaster) + { + config.wired_sync_mode = K4A_WIRED_SYNC_MODE_MASTER; + } + + else if (asSubordinate) + { + config.wired_sync_mode = K4A_WIRED_SYNC_MODE_SUBORDINATE; + //Sets the offset on subordinate devices. Should be a multiple of 160, each subordinate having a different multiplier in ascending order. + //This is very important, as it avoids firing the Kinects Lasers at the same time. + + config.subordinate_delay_off_master_usec = 0; + + config.subordinate_delay_off_master_usec = 160 * syncOffsetMultiplier; + } + + else + { + config.wired_sync_mode = K4A_WIRED_SYNC_MODE_STANDALONE; + } + // Start the camera with the given configuration bInitialized = K4A_SUCCEEDED(k4a_device_start_cameras(kinectSensor, &config)); @@ -49,31 +77,51 @@ bool AzureKinectCapture::Initialize() bInitialized = false; return bInitialized; } + transformation = k4a_transformation_create(&calibration); - std::chrono::time_point start = std::chrono::system_clock::now(); - bool bTemp; - do + //If this device is a subordinate, it is expected to start capturing at a later time (When the master has started), so we skip this check + if (!asSubordinate) { - bTemp = AcquireFrame(); - - std::chrono::duration elapsedSeconds = std::chrono::system_clock::now() - start; - if (elapsedSeconds.count() > 5.0) + std::chrono::time_point start = std::chrono::system_clock::now(); + bool bTemp; + do { - bInitialized = false; - break; - } - } while (!bTemp); + bTemp = AcquireFrame(); + + std::chrono::duration elapsedSeconds = std::chrono::system_clock::now() - start; + if (elapsedSeconds.count() > 5.0) + { + bInitialized = false; + break; + } + } while (!bTemp); + } size_t serialNoSize; k4a_device_get_serialnum(kinectSensor, NULL, &serialNoSize); serialNumber = std::string(serialNoSize, '\0'); k4a_device_get_serialnum(kinectSensor, (char*)serialNumber.c_str(), &serialNoSize); - return bInitialized; } +bool AzureKinectCapture::Close() +{ + if (!bInitialized) + { + return false; + } + + k4a_device_stop_cameras(kinectSensor); + k4a_device_close(kinectSensor); + + bInitialized = false; + + + return true; +} + bool AzureKinectCapture::AcquireFrame() { if (!bInitialized) @@ -96,6 +144,9 @@ bool AzureKinectCapture::AcquireFrame() colorImage = k4a_capture_get_color_image(capture); depthImage = k4a_capture_get_depth_image(capture); + + currentTimeStamp = k4a_image_get_device_timestamp_usec(colorImage); + if (colorImage == NULL || depthImage == NULL) { k4a_capture_release(capture); @@ -119,6 +170,7 @@ bool AzureKinectCapture::AcquireFrame() memcpy(pColorRGBX, k4a_image_get_buffer(colorImage), nColorFrameWidth * nColorFrameHeight * sizeof(RGB)); memcpy(pDepth, k4a_image_get_buffer(depthImage), nDepthFrameHeight * nDepthFrameWidth * sizeof(UINT16)); + k4a_capture_release(capture); return true; @@ -260,4 +312,42 @@ void AzureKinectCapture::MapColorFrameToDepthSpace(RGB *pColorInDepthSpace) k4a_transformation_color_image_to_depth_camera(transformation, depthImage, colorImage, colorImageInDepth); memcpy(pColorInDepthSpace, k4a_image_get_buffer(colorImageInDepth), nDepthFrameHeight * nDepthFrameWidth * 4 * (int)sizeof(uint8_t)); -} \ No newline at end of file +} + + +/// +/// Determines if this camera is configured as a Master, Subordinate or Standalone. +/// This is achieved by looking at how the sync out and sync in ports of the device are connected +/// +/// Returns int -1 for Subordinate, int 0 for Master and int 1 for Standalone +int AzureKinectCapture::GetSyncJackState() +{ + if (K4A_RESULT_SUCCEEDED == k4a_device_get_sync_jack(kinectSensor, &syncInConnected, &syncOutConnected)) + { + if (syncInConnected) + { + return -1; //Device is Subordinate, as it recieves a signal via its "Sync In" Port + } + + else if (!syncInConnected && syncOutConnected) + { + return 0; //Device is Master, as it doens't recieve a signal from its "Sync In" Port, but sends one through its "Sync Out" Port + } + + else + { + return 1; //Device is Standalone, as it doesn't have a valid cabel configuration on its Sync Ports + } + } + + else + { + return 1; //Probably failed because there are no cabels connected, this means the device should be set as standalone + } +} + +uint64_t AzureKinectCapture::GetTimeStamp() +{ + return currentTimeStamp; +} + diff --git a/src/LiveScanClient/frameFileWriterReader.cpp b/src/LiveScanClient/frameFileWriterReader.cpp index fe33503..fbb3259 100644 --- a/src/LiveScanClient/frameFileWriterReader.cpp +++ b/src/LiveScanClient/frameFileWriterReader.cpp @@ -86,7 +86,7 @@ bool FrameFileWriterReader::readFrame(std::vector &outPoints, std::vect } -void FrameFileWriterReader::writeFrame(std::vector points, std::vector colors) +void FrameFileWriterReader::writeFrame(std::vector points, std::vector colors, uint64_t timestamp) { if (!m_bFileOpenedForWriting) openNewFileForWriting(); @@ -94,7 +94,8 @@ void FrameFileWriterReader::writeFrame(std::vector points, std::vector< FILE *f = m_pFileHandle; int nPoints = static_cast(points.size()); - fprintf(f, "n_points= %d\nframe_timestamp= %d\n", nPoints, getRecordingTimeMilliseconds()); + //fprintf(f, "n_points= %d\nframe_timestamp= %d\n", nPoints, getRecordingTimeMilliseconds()); + fprintf(f, "n_points= %d\nframe_timestamp= %d\n", nPoints, timestamp); if (nPoints > 0) { fwrite((void*)points.data(), sizeof(points[0]), nPoints, f); diff --git a/src/LiveScanClient/liveScanClient.cpp b/src/LiveScanClient/liveScanClient.cpp index f3d69a1..19c2c9c 100644 --- a/src/LiveScanClient/liveScanClient.cpp +++ b/src/LiveScanClient/liveScanClient.cpp @@ -206,7 +206,8 @@ void LiveScanClient::UpdateFrame() if (m_bCaptureFrame) { - m_framesFileWriterReader.writeFrame(m_vLastFrameVertices, m_vLastFrameRGB); + uint64_t timeStamp = pCapture->GetTimeStamp(); + m_framesFileWriterReader.writeFrame(m_vLastFrameVertices, m_vLastFrameRGB, timeStamp); m_bConfirmCaptured = true; m_bCaptureFrame = false; } @@ -275,8 +276,8 @@ LRESULT CALLBACK LiveScanClient::DlgProc(HWND hWnd, UINT message, WPARAM wParam, // Init Direct2D D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory); - // Get and initialize the default Kinect sensor - bool res = pCapture->Initialize(); + // Get and initialize the default Kinect sensor as standalone + bool res = pCapture->Initialize(false, false, 0); if (res) { calibration.LoadCalibration(pCapture->serialNumber); @@ -444,16 +445,62 @@ void LiveScanClient::HandleSocket() //calibrate else if (received[i] == MSG_CALIBRATE) m_bCalibrate = true; - //Set this client as master - else if (received[i] == MSG_SET_MASTER) { - m_bIsMaster = true; - m_bIsSubOrdinate = false; + + //Enables Temporal sync on this client + else if (received[i] == MSG_SET_TEMPSYNC_ON) { + + i++; //Get next byte (the sync Offset) + int syncOffset = received[i]; + + //Determine if this device is a subordinate, master, or standalone + int jackState = pCapture->GetSyncJackState(); + + switch (jackState) + { + case -1: + currentTempSyncState = SUBORDINATE; + //Restart this device as Subordinate, with a unique syncOffset (send by the server) + pCapture->Close(); + pCapture->Initialize(false, true, syncOffset); + //Confirm to the server, that we set this device as subordinate + m_bConfirmTempSyncState = true; + break; + + case 0: + currentTempSyncState = MASTER; + //Only Close this device, as it needs to wait for all subordinates to start, before starting itself + pCapture->Close(); + m_bConfirmTempSyncState = true; + break; + + case 1://Device is Standalone + currentTempSyncState = STANDALONE; + //Restart this device as Standalone + pCapture->Close(); + pCapture->Initialize(false, false, 0); + m_bConfirmTempSyncState = true; + break; + default: + break; + } } - //Set this client as subordinate - else if (received[i] == MSG_SET_SUBORDINATE) { - m_bIsMaster = false; - m_bIsSubOrdinate = true; - } + + //Sets this device as Standalone + else if (received[i] == MSG_SET_TEMPSYNC_OFF) { + currentTempSyncState = STANDALONE; + pCapture->Close(); + pCapture->Initialize(false, false, 0); + m_bConfirmTempSyncState = true; + } + + //Got confirmation from the server that all subs have started, and we can now start the master + else if (received[i] == MSG_START_MASTER) { + if (currentTempSyncState == MASTER) + { + pCapture->Initialize(true, false, 0); + } + } + //receive settings //TODO: what if packet is split? else if (received[i] == MSG_RECEIVE_SETTINGS) @@ -577,24 +624,25 @@ void LiveScanClient::HandleSocket() m_bConfirmCaptured = false; } - if (m_bConfirmMaster) + //Send validation to the server that this device has been set to a specific Sync State + if (m_bConfirmTempSyncState) { - int size = 2; - char *buffer = new char[size]; - buffer[0] = MSG_CONFIRM_TEMP_SYNC_STATUS; - buffer[1] = 0; - m_pClientSocket->SendBytes(buffer, size); - } - - if (m_bConfirmSubOrdinate) - { - int size = 2; + int size = 3; //Somehow it doesn't work when sending only two bytes?? (So we send an extra one) char* buffer = new char[size]; buffer[0] = MSG_CONFIRM_TEMP_SYNC_STATUS; - buffer[1] = -1; + + switch (currentTempSyncState) + { + case SUBORDINATE: buffer[1] = 0; break; + case MASTER: buffer[1] = 1; break; + case STANDALONE: buffer[1] = 2; break; + } + m_pClientSocket->SendBytes(buffer, size); + m_bConfirmTempSyncState = false; } + if (m_bConfirmCalibrated) { int size = (9 + 3) * sizeof(float) + sizeof(int) + 1; From 2b15ed0f5bd2a43e3819d22b08b585bb4712f449 Mon Sep 17 00:00:00 2001 From: Christopher Remde Date: Wed, 8 Jul 2020 14:15:49 +0200 Subject: [PATCH 3/6] Cleaned up --- LiveScanServer/KinectServer.cs | 2 +- LiveScanServer/KinectSocket.cs | 12 ------------ src/LiveScanClient/azureKinectCapture.cpp | 5 +---- src/LiveScanClient/frameFileWriterReader.cpp | 3 ++- 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/LiveScanServer/KinectServer.cs b/LiveScanServer/KinectServer.cs index 1d7d69c..d9c38d7 100644 --- a/LiveScanServer/KinectServer.cs +++ b/LiveScanServer/KinectServer.cs @@ -234,7 +234,7 @@ namespace KinectServer } /// - /// Send temporal Sync Data to all client + /// Send temporal Sync Data to all clients /// public void SendTemporalSyncData() { diff --git a/LiveScanServer/KinectSocket.cs b/LiveScanServer/KinectSocket.cs index 7d1f9b1..f2fb4a0 100644 --- a/LiveScanServer/KinectSocket.cs +++ b/LiveScanServer/KinectSocket.cs @@ -220,18 +220,6 @@ namespace KinectServer currentTempSyncState = eTempSyncConfig.STANDALONE; } - //MARKED FOR DELETION - //if(currentTempSyncState == eTempSyncConfig.SUBORDINATE && confirmSubStarted == 1) - //{ - - //} - - //else if(currentTempSyncState == eTempSyncConfig.SUBORDINATE && confirmSubStarted == 0) - //{ - // bSubStarted = false; - // eSubInitialized?.Invoke(); - //} - UpdateSocketState(); } diff --git a/src/LiveScanClient/azureKinectCapture.cpp b/src/LiveScanClient/azureKinectCapture.cpp index 06798fe..a69a291 100644 --- a/src/LiveScanClient/azureKinectCapture.cpp +++ b/src/LiveScanClient/azureKinectCapture.cpp @@ -56,10 +56,7 @@ bool AzureKinectCapture::Initialize(bool asMaster, bool asSubordinate, int syncO { config.wired_sync_mode = K4A_WIRED_SYNC_MODE_SUBORDINATE; //Sets the offset on subordinate devices. Should be a multiple of 160, each subordinate having a different multiplier in ascending order. - //This is very important, as it avoids firing the Kinects Lasers at the same time. - - config.subordinate_delay_off_master_usec = 0; - + //This is very important, as it avoids firing the Kinects lasers at the same time. config.subordinate_delay_off_master_usec = 160 * syncOffsetMultiplier; } diff --git a/src/LiveScanClient/frameFileWriterReader.cpp b/src/LiveScanClient/frameFileWriterReader.cpp index fbb3259..c1a4f7f 100644 --- a/src/LiveScanClient/frameFileWriterReader.cpp +++ b/src/LiveScanClient/frameFileWriterReader.cpp @@ -94,7 +94,8 @@ void FrameFileWriterReader::writeFrame(std::vector points, std::vector< FILE *f = m_pFileHandle; int nPoints = static_cast(points.size()); - //fprintf(f, "n_points= %d\nframe_timestamp= %d\n", nPoints, getRecordingTimeMilliseconds()); + + //The Timestamp is generated by the Kinect instead of the system. If temporal Sync is enabled, Master and Subordinate have a synced timestamp fprintf(f, "n_points= %d\nframe_timestamp= %d\n", nPoints, timestamp); if (nPoints > 0) { From 76bf5e9dba87aac98a9c2d7ac723ae77d89bf2ce Mon Sep 17 00:00:00 2001 From: Christopher Remde Date: Wed, 14 Oct 2020 23:21:18 +0200 Subject: [PATCH 4/6] Added new UI for the Temporal Sync Feature, made the software more robust including: -Settings window can only be opened once -When the client can't open a device, it tries multiple times -Recording bins are now saved with a unique ID per device, to allow multiple clients to record on the same PC -Synchronized & Unsynchronized devices can be both used in one server -Included a workaround for a bug in the Azure Kinect SDK regarding exposure (microsoft/Azure-Kinect-Sensor-SDK#1378) --- LiveScanPlayer/FrameFileReaderBin.cs | 2 - LiveScanServer/KinectServer.cs | 170 ++++++++++++++++-- LiveScanServer/KinectSettings.cs | 14 +- LiveScanServer/KinectSocket.cs | 98 +++++++++- LiveScanServer/MainWindowForm.cs | 24 ++- LiveScanServer/SettingsForm.Designer.cs | 164 ++++++++++++----- LiveScanServer/SettingsForm.cs | 110 ++++++++++-- include/LiveScanClient/azureKinectCapture.h | 18 +- .../LiveScanClient/frameFileWriterReader.h | 4 +- include/LiveScanClient/iCapture.h | 5 +- include/LiveScanClient/liveScanClient.h | 6 +- include/LiveScanClient/utils.h | 14 +- src/LiveScanClient/azureKinectCapture.cpp | 104 ++++++++++- src/LiveScanClient/frameFileWriterReader.cpp | 8 +- src/LiveScanClient/liveScanClient.cpp | 132 ++++++++++++-- 15 files changed, 744 insertions(+), 129 deletions(-) diff --git a/LiveScanPlayer/FrameFileReaderBin.cs b/LiveScanPlayer/FrameFileReaderBin.cs index 371f46e..8b98ff6 100644 --- a/LiveScanPlayer/FrameFileReaderBin.cs +++ b/LiveScanPlayer/FrameFileReaderBin.cs @@ -46,8 +46,6 @@ namespace LiveScanPlayer lineParts = ReadLine().Split(' '); int frameTimestamp = Int32.Parse(lineParts[1]); - Console.WriteLine((frameTimestamp / 1000).ToString()); - short[] tempVertices = new short[3 * nPoints]; byte[] tempColors = new byte[4 * nPoints]; diff --git a/LiveScanServer/KinectServer.cs b/LiveScanServer/KinectServer.cs index d9c38d7..91adb7d 100644 --- a/LiveScanServer/KinectServer.cs +++ b/LiveScanServer/KinectServer.cs @@ -22,6 +22,7 @@ using System.Threading; using System.Net.Sockets; using System.Net; using System.ComponentModel; +using System.Windows.Forms; namespace KinectServer { @@ -31,9 +32,15 @@ namespace KinectServer Socket oServerSocket; bool bServerRunning = false; - bool waitForSubToStart = false; + bool bWaitForSubToStart = false; + + //This lock prevents the user from enabeling/disabling the Temp Sync State while the cameras are in transition to another state. + //When starting the server, all devices are already initialized, as the LiveScanClient can only connect with an initialized device + bool allDevicesInitialized = true; KinectSettings oSettings; + SettingsForm fSettingsForm; + MainWindowForm fMainWindowForm; object oClientSocketLock = new object(); object oFrameRequestLock = new object(); @@ -133,6 +140,21 @@ namespace KinectServer this.oSettings = settings; } + public void SetSettingsForm(SettingsForm settings) + { + fSettingsForm = settings; + } + + public void SetMainWindowForm(MainWindowForm main) + { + fMainWindowForm = main; + } + + public SettingsForm GetSettingsForm() + { + return fSettingsForm; + } + private void SocketListChanged() { if (eSocketListChanged != null) @@ -226,6 +248,7 @@ namespace KinectServer { lock (oClientSocketLock) { + for (int i = 0; i < lClientSockets.Count; i++) { lClientSockets[i].SendCalibrationData(); @@ -234,33 +257,115 @@ namespace KinectServer } /// - /// Send temporal Sync Data to all clients + /// Request the device Sync state, so that we know which Device is Master and which are Subordinates before starting them + /// + public void RequestDeviceSyncState() + { + lock (oClientSocketLock) + { + for (int i = 0; i < lClientSockets.Count; i++) + { + lClientSockets[i].RequestTempSyncState(); + } + } + } + + /// + /// When a client has send its Device Sync State, we try to Set the Client Sync State /// public void SendTemporalSyncData() { lock (oClientSocketLock) { - if(lClientSockets.Count > 1 && oSettings.bEnableSync) - { - byte syncOffSetCounter = 1; + int masterCount = 0; + int subordinateCount = 0; - for (int i = 0; i < lClientSockets.Count; i++) + //First we check if we have recieved the Device Sync State of all Devices + //If not, we return and the next client who confirms their state starts this function again + for (int i = 0; i < lClientSockets.Count; i++) + { + if(lClientSockets[i].currentDeviceTempSyncState == KinectSocket.eTempSyncConfig.MASTER) + { + masterCount++; + } + + if (lClientSockets[i].currentDeviceTempSyncState == KinectSocket.eTempSyncConfig.SUBORDINATE) + { + subordinateCount++; + } + + if (lClientSockets[i].currentDeviceTempSyncState == KinectSocket.eTempSyncConfig.UNKNOWN) + { + return; + } + } + + //On a second step we check if we have exactly one master and at least one subordinate + + if(masterCount != 1 || subordinateCount < 1) + { + //If not, we show a error message and disable the temporal sync + + fMainWindowForm?.SetStatusBarOnTimer("Temporal Sync cables not connected properly", 5000); + fSettingsForm?.ActivateTempSyncEnableButton(); + return; + } + + + allDevicesInitialized = false; + + byte syncOffSetCounter = 0; + + for (int i = 0; i < lClientSockets.Count; i++) + { + if(lClientSockets[i].currentDeviceTempSyncState == KinectSocket.eTempSyncConfig.SUBORDINATE) { - lClientSockets[i].SendTemporalSyncStatus(true, syncOffSetCounter); syncOffSetCounter++; } - waitForSubToStart = true; + lClientSockets[i].SendTemporalSyncStatus(true, syncOffSetCounter); + } - else + bWaitForSubToStart = true; + } + } + + + /// + /// Sets all clients as standalone + /// + public void DisableTemporalSync() + { + allDevicesInitialized = false; + + lock (oClientSocketLock) + { + for (int i = 0; i < lClientSockets.Count; i++) { - for (int i = 0; i < lClientSockets.Count; i++) + lClientSockets[i].SendTemporalSyncStatus(false, 0); + } + } + } + + + public void ConfirmTemporalSyncDisabled() + { + if (bWaitForSubToStart) + return; + + lock (oClientSocketLock) + { + for (int i = 0; i < lClientSockets.Count; i++) + { + if (lClientSockets[i].currentClientTempSyncState != KinectSocket.eTempSyncConfig.STANDALONE) { - lClientSockets[i].SendTemporalSyncStatus(false, 0); + return; } } } + + allDevicesInitialized = true; } /// @@ -268,7 +373,7 @@ namespace KinectServer /// public void CheckForMasterStart() { - if (!oSettings.bEnableSync || !waitForSubToStart) + if (!bWaitForSubToStart) { return; } @@ -279,23 +384,40 @@ namespace KinectServer { for (int i = 0; i < lClientSockets.Count; i++) { - if (!lClientSockets[i].bSubStarted && lClientSockets[i].currentTempSyncState == KinectSocket.eTempSyncConfig.SUBORDINATE) + if (!lClientSockets[i].bSubStarted && lClientSockets[i].currentClientTempSyncState == KinectSocket.eTempSyncConfig.SUBORDINATE) { allSubsStarted = false; break; } } + bWaitForSubToStart = false; + if (allSubsStarted) { for (int i = 0; i < lClientSockets.Count; i++) { - lClientSockets[i].SendMasterInitialize(); + if(lClientSockets[i].currentDeviceTempSyncState == KinectSocket.eTempSyncConfig.MASTER) + { + lClientSockets[i].SendMasterInitialize(); + return; + } } } - } + } + } - + /// + /// Tells the server that it is now ok to start recieving user changes again + /// + public void MasterSuccessfullyRestarted() + { + allDevicesInitialized = true; + } + + public bool GetAllDevicesInitialized() + { + return allDevicesInitialized; } public bool GetStoredFrame(List> lFramesRGB, List> lFramesVerts) @@ -369,6 +491,7 @@ namespace KinectServer //Wait till frames received bool allGathered = false; + while (!allGathered) { allGathered = true; @@ -384,6 +507,7 @@ namespace KinectServer } } } + } //Store received frames @@ -396,6 +520,7 @@ namespace KinectServer lFramesBody.Add(new List(lClientSockets[i].lBodies)); } } + } } @@ -427,6 +552,9 @@ namespace KinectServer lClientSockets[lClientSockets.Count - 1].SendSettings(oSettings); lClientSockets[lClientSockets.Count - 1].eChanged += new SocketChangedHandler(SocketListChanged); lClientSockets[lClientSockets.Count - 1].eSubInitialized += new SubOrdinateInitialized(CheckForMasterStart); + lClientSockets[lClientSockets.Count - 1].eMasterRestart += new MasterRestarted(MasterSuccessfullyRestarted); + lClientSockets[lClientSockets.Count - 1].eSyncJackstate += new RecievedSyncJackState(SendTemporalSyncData); + lClientSockets[lClientSockets.Count - 1].eStandAloneInitialized += new StandAloneInitialized(ConfirmTemporalSyncDisabled); if (eSocketListChanged != null) { @@ -509,6 +637,16 @@ namespace KinectServer lClientSockets[i].ReceiveTemporalSyncStatus(); } + else if(buffer[0] == 5) + { + lClientSockets[i].RecieveMasterHasRestarted(); + } + + else if(buffer[0] == 6) + { + lClientSockets[i].RecieveDeviceSyncState(); + } + buffer = lClientSockets[i].Receive(1); } } diff --git a/LiveScanServer/KinectSettings.cs b/LiveScanServer/KinectSettings.cs index 987e845..9d42a7e 100644 --- a/LiveScanServer/KinectSettings.cs +++ b/LiveScanServer/KinectSettings.cs @@ -42,7 +42,11 @@ namespace KinectServer public bool bMergeScansForSave = true; public bool bSaveAsBinaryPLY = true; - public bool bEnableSync = false; + public bool bSyncEnabled = false; + + public bool bAutoExposureEnabled = true; + + public int nExposureStep = -8; public KinectSettings() { @@ -102,6 +106,14 @@ namespace KinectServer bTemp = BitConverter.GetBytes(iCompressionLevel); lData.AddRange(bTemp); + if (bAutoExposureEnabled) + lData.Add(1); + else + lData.Add(0); + + bTemp = BitConverter.GetBytes(nExposureStep); + lData.AddRange(bTemp); + return lData; } } diff --git a/LiveScanServer/KinectSocket.cs b/LiveScanServer/KinectSocket.cs index f2fb4a0..5104381 100644 --- a/LiveScanServer/KinectSocket.cs +++ b/LiveScanServer/KinectSocket.cs @@ -18,12 +18,15 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using System.Net.Sockets; - +using OpenTK.Graphics; namespace KinectServer { public delegate void SocketChangedHandler(); public delegate void SubOrdinateInitialized(); + public delegate void MasterRestarted(); + public delegate void RecievedSyncJackState(); + public delegate void StandAloneInitialized(); public class KinectSocket { @@ -35,11 +38,19 @@ namespace KinectServer public bool bNoMoreStoredFrames = true; public bool bCalibrated = false; public bool bSubStarted = false; + public bool bMasterStarted = false; + + public enum eTempSyncConfig { MASTER, SUBORDINATE, STANDALONE, UNKNOWN } + + //Shows how the *Device* is configured (Determined only by the Sync-Cables hooked up to the device) + public eTempSyncConfig currentDeviceTempSyncState = eTempSyncConfig.STANDALONE; + + //Shows how the Client-Software is configured (This is set by the server) + public eTempSyncConfig currentClientTempSyncState = eTempSyncConfig.STANDALONE; - public enum eTempSyncConfig { MASTER, SUBORDINATE, STANDALONE } - public eTempSyncConfig currentTempSyncState = eTempSyncConfig.STANDALONE; //The pose of the sensor in the scene (used by the OpenGLWindow to show the sensor) public AffineTransform oCameraPose = new AffineTransform(); + //The transform that maps the vertices in the sensor coordinate system to the world corrdinate system. public AffineTransform oWorldTransform = new AffineTransform(); @@ -51,6 +62,9 @@ namespace KinectServer public event SocketChangedHandler eChanged; public event SubOrdinateInitialized eSubInitialized; + public event MasterRestarted eMasterRestart; + public event RecievedSyncJackState eSyncJackstate; + public event StandAloneInitialized eStandAloneInitialized; public KinectSocket(Socket clientSocket) { @@ -135,7 +149,10 @@ namespace KinectServer /// Enable/Disable Temporal Sync /// Only set when this device should be a subordinate. Should be a unique number in ascending order for each sub public void SendTemporalSyncStatus(bool tempSyncOn, byte syncOffSetMultiplier) - { + { + bSubStarted = false; + currentClientTempSyncState = eTempSyncConfig.UNKNOWN; + if (tempSyncOn) { byte[] data = new byte[2]; @@ -161,6 +178,49 @@ namespace KinectServer SendByte(); } + + /// + /// Asks the client in which way the Sync-Cables are hooked up to the device + /// + public void RequestTempSyncState() + { + currentDeviceTempSyncState = eTempSyncConfig.UNKNOWN; + byteToSend[0] = 10; + SendByte(); + } + + public void RecieveDeviceSyncState() + { + byte tempSyncState; + byte[] buffer = new byte[1024]; + + while (oSocket.Available == 0) + { + if (!SocketConnected()) + return; + } + + oSocket.Receive(buffer, 3, SocketFlags.None); + tempSyncState = buffer[0]; + + if (tempSyncState == 0) + { + currentDeviceTempSyncState = eTempSyncConfig.SUBORDINATE; + } + + else if (tempSyncState == 1) + { + currentDeviceTempSyncState = eTempSyncConfig.MASTER; + } + + else if (tempSyncState == 2) + { + currentDeviceTempSyncState = eTempSyncConfig.STANDALONE; + } + + eSyncJackstate.Invoke(); + } + public void ReceiveCalibrationData() { bCalibrated = true; @@ -205,25 +265,36 @@ namespace KinectServer if (tempSyncState == 0) { - currentTempSyncState = eTempSyncConfig.SUBORDINATE; + currentClientTempSyncState = eTempSyncConfig.SUBORDINATE; bSubStarted = true; + bMasterStarted = false; eSubInitialized?.Invoke(); } else if(tempSyncState == 1) { - currentTempSyncState = eTempSyncConfig.MASTER; + currentClientTempSyncState = eTempSyncConfig.MASTER; + bSubStarted = false; } else if (tempSyncState == 2) { - currentTempSyncState = eTempSyncConfig.STANDALONE; + currentClientTempSyncState = eTempSyncConfig.STANDALONE; + bSubStarted = false; + bMasterStarted = false; + eStandAloneInitialized?.Invoke(); } UpdateSocketState(); } + public void RecieveMasterHasRestarted() + { + bMasterStarted = true; + eMasterRestart?.Invoke(); + } + public void ReceiveFrame() { lFrameRGB.Clear(); @@ -241,6 +312,9 @@ namespace KinectServer oSocket.Receive(buffer, 8, SocketFlags.None); nToRead = BitConverter.ToInt32(buffer, 0); + + + int iCompressed = BitConverter.ToInt32(buffer, 4); if (nToRead == -1) @@ -249,6 +323,14 @@ namespace KinectServer return; } + //Sometimes we recieve negative values when the cameras are restarting, I don't know why yet. + //I'm just patching this up for now.s + + if (nToRead <= 0) + { + return; + } + buffer = new byte[nToRead]; int nAlreadyRead = 0; @@ -371,7 +453,7 @@ namespace KinectServer { string tempSyncMessage = ""; - switch (currentTempSyncState) + switch (currentClientTempSyncState) { case eTempSyncConfig.MASTER: tempSyncMessage = "[MASTER]"; diff --git a/LiveScanServer/MainWindowForm.cs b/LiveScanServer/MainWindowForm.cs index 7b7fdc2..09ad823 100644 --- a/LiveScanServer/MainWindowForm.cs +++ b/LiveScanServer/MainWindowForm.cs @@ -75,8 +75,6 @@ namespace KinectServer IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); Stream stream = new FileStream("settings.bin", FileMode.Open, FileAccess.Read); oSettings = (KinectSettings)formatter.Deserialize(stream); - //This always needs to be set to false on startup, as we can only activate it when all Kinects are already connected - oSettings.bEnableSync = false; stream.Close(); } catch(Exception) @@ -127,10 +125,16 @@ namespace KinectServer //Opens the settings form private void btSettings_Click(object sender, EventArgs e) { - SettingsForm form = new SettingsForm(); - form.oSettings = oSettings; - form.oServer = oServer; - form.Show(); + if(oServer.GetSettingsForm() == null) + { + SettingsForm form = new SettingsForm(); + form.oSettings = oSettings; + form.oServer = oServer; + form.Show(); + oServer.SetSettingsForm(form); + oServer.SetMainWindowForm(this); + } + } //Performs recording which is synchronized frame capture. @@ -265,6 +269,12 @@ namespace KinectServer while (!worker.CancellationPending) { Thread.Sleep(1); + + if (!oServer.GetAllDevicesInitialized()) + { + continue; + } + oServer.GetLatestFrame(lFramesRGB, lFramesVerts, lFramesBody); //Update the vertex and color lists that are common between this class and the OpenGLWindow. @@ -474,7 +484,7 @@ namespace KinectServer OpenGLWorker.RunWorkerAsync(); } - private void SetStatusBarOnTimer(string message, int milliseconds) + public void SetStatusBarOnTimer(string message, int milliseconds) { statusLabel.Text = message; diff --git a/LiveScanServer/SettingsForm.Designer.cs b/LiveScanServer/SettingsForm.Designer.cs index 0387c58..99358a5 100644 --- a/LiveScanServer/SettingsForm.Designer.cs +++ b/LiveScanServer/SettingsForm.Designer.cs @@ -33,9 +33,15 @@ this.lbICPIters = new System.Windows.Forms.Label(); this.txtICPIters = new System.Windows.Forms.TextBox(); this.grClient = new System.Windows.Forms.GroupBox(); - this.grSync = new System.Windows.Forms.GroupBox(); + this.grExposure = new System.Windows.Forms.GroupBox(); + this.lbManualExposure = new System.Windows.Forms.Label(); + this.chAutoExposureEnabled = new System.Windows.Forms.CheckBox(); + this.trManualExposure = new System.Windows.Forms.TrackBar(); + this.grTempSync = new System.Windows.Forms.GroupBox(); + this.btSyncDisable = new System.Windows.Forms.Button(); + this.btSyncEnable = new System.Windows.Forms.Button(); + this.lbTempSync = new System.Windows.Forms.Label(); this.label1 = new System.Windows.Forms.Label(); - this.chSync = new System.Windows.Forms.CheckBox(); this.grBody = new System.Windows.Forms.GroupBox(); this.chSkeletons = new System.Windows.Forms.CheckBox(); this.chBodyData = new System.Windows.Forms.CheckBox(); @@ -82,9 +88,10 @@ this.rAsciiPly = new System.Windows.Forms.RadioButton(); this.txtRefinIters = new System.Windows.Forms.TextBox(); this.lbOuterIters = new System.Windows.Forms.Label(); - this.label3 = new System.Windows.Forms.Label(); this.grClient.SuspendLayout(); - this.grSync.SuspendLayout(); + this.grExposure.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.trManualExposure)).BeginInit(); + this.grTempSync.SuspendLayout(); this.grBody.SuspendLayout(); this.grMarkers.SuspendLayout(); this.grBounding.SuspendLayout(); @@ -131,30 +138,108 @@ // // grClient // - this.grClient.Controls.Add(this.grSync); + this.grClient.Controls.Add(this.grExposure); + this.grClient.Controls.Add(this.grTempSync); this.grClient.Controls.Add(this.grBody); this.grClient.Controls.Add(this.grMarkers); this.grClient.Controls.Add(this.grBounding); this.grClient.Controls.Add(this.grFiltering); this.grClient.Location = new System.Drawing.Point(12, 12); this.grClient.Name = "grClient"; - this.grClient.Size = new System.Drawing.Size(632, 291); + this.grClient.Size = new System.Drawing.Size(632, 408); this.grClient.TabIndex = 43; this.grClient.TabStop = false; this.grClient.Text = "KinectClient settings"; // - // grSync + // grExposure // - this.grSync.Controls.Add(this.label3); - this.grSync.Controls.Add(this.label1); - this.grSync.Controls.Add(this.chSync); - this.grSync.Location = new System.Drawing.Point(271, 211); - this.grSync.Name = "grSync"; - this.grSync.Size = new System.Drawing.Size(355, 80); - this.grSync.TabIndex = 48; - this.grSync.TabStop = false; - this.grSync.Text = "Temporal Sync"; - this.grSync.Enter += new System.EventHandler(this.grSync_Enter); + this.grExposure.Controls.Add(this.lbManualExposure); + this.grExposure.Controls.Add(this.chAutoExposureEnabled); + this.grExposure.Controls.Add(this.trManualExposure); + this.grExposure.Location = new System.Drawing.Point(9, 283); + this.grExposure.Name = "grExposure"; + this.grExposure.Size = new System.Drawing.Size(249, 115); + this.grExposure.TabIndex = 49; + this.grExposure.TabStop = false; + this.grExposure.Text = "Exposure Settings"; + // + // lbManualExposure + // + this.lbManualExposure.AutoSize = true; + this.lbManualExposure.Location = new System.Drawing.Point(79, 52); + this.lbManualExposure.Name = "lbManualExposure"; + this.lbManualExposure.Size = new System.Drawing.Size(91, 13); + this.lbManualExposure.TabIndex = 7; + this.lbManualExposure.Text = "Manual exposure:"; + // + // chAutoExposureEnabled + // + this.chAutoExposureEnabled.AutoSize = true; + this.chAutoExposureEnabled.Checked = true; + this.chAutoExposureEnabled.CheckState = System.Windows.Forms.CheckState.Checked; + this.chAutoExposureEnabled.Location = new System.Drawing.Point(11, 20); + this.chAutoExposureEnabled.Name = "chAutoExposureEnabled"; + this.chAutoExposureEnabled.Size = new System.Drawing.Size(135, 17); + this.chAutoExposureEnabled.TabIndex = 6; + this.chAutoExposureEnabled.Text = "Auto exposure enabled"; + this.chAutoExposureEnabled.UseVisualStyleBackColor = true; + this.chAutoExposureEnabled.CheckedChanged += new System.EventHandler(this.chAutoExposureEnabled_CheckedChanged); + // + // trManualExposure + // + this.trManualExposure.Enabled = false; + this.trManualExposure.LargeChange = 1; + this.trManualExposure.Location = new System.Drawing.Point(6, 62); + this.trManualExposure.Maximum = -5; + this.trManualExposure.Minimum = -11; + this.trManualExposure.Name = "trManualExposure"; + this.trManualExposure.Size = new System.Drawing.Size(237, 45); + this.trManualExposure.TabIndex = 5; + this.trManualExposure.Value = -8; + this.trManualExposure.Scroll += new System.EventHandler(this.trManualExposure_Scroll); + // + // grTempSync + // + this.grTempSync.Controls.Add(this.btSyncDisable); + this.grTempSync.Controls.Add(this.btSyncEnable); + this.grTempSync.Controls.Add(this.lbTempSync); + this.grTempSync.Controls.Add(this.label1); + this.grTempSync.Location = new System.Drawing.Point(271, 211); + this.grTempSync.Name = "grTempSync"; + this.grTempSync.Size = new System.Drawing.Size(355, 97); + this.grTempSync.TabIndex = 48; + this.grTempSync.TabStop = false; + this.grTempSync.Text = "Temporal Sync"; + // + // btSyncDisable + // + this.btSyncDisable.Location = new System.Drawing.Point(104, 19); + this.btSyncDisable.Name = "btSyncDisable"; + this.btSyncDisable.Size = new System.Drawing.Size(92, 23); + this.btSyncDisable.TabIndex = 4; + this.btSyncDisable.Text = "Disable"; + this.btSyncDisable.UseVisualStyleBackColor = true; + this.btSyncDisable.Click += new System.EventHandler(this.btSyncDisable_click); + // + // btSyncEnable + // + this.btSyncEnable.Location = new System.Drawing.Point(6, 19); + this.btSyncEnable.Name = "btSyncEnable"; + this.btSyncEnable.Size = new System.Drawing.Size(92, 23); + this.btSyncEnable.TabIndex = 3; + this.btSyncEnable.Text = "Enable"; + this.btSyncEnable.UseVisualStyleBackColor = true; + this.btSyncEnable.Click += new System.EventHandler(this.btSyncEnable_click); + // + // lbTempSync + // + this.lbTempSync.Location = new System.Drawing.Point(3, 45); + this.lbTempSync.Name = "lbTempSync"; + this.lbTempSync.Size = new System.Drawing.Size(342, 47); + this.lbTempSync.TabIndex = 2; + this.lbTempSync.Text = "Before activating, make sure that all Kinects are connected to the server and pro" + + "perly connected via the sync cables. When Temporal Sync is activated, the exposu" + + "re needs to be set manually!"; // // label1 // @@ -166,17 +251,6 @@ this.label1.TabIndex = 1; this.label1.Text = "Notice: All devices need to be connected to the server and via sync cabel before" + " enabeling! First Device in Server List will become the master"; - // - // chSync - // - this.chSync.AutoSize = true; - this.chSync.Location = new System.Drawing.Point(6, 19); - this.chSync.Name = "chSync"; - this.chSync.Size = new System.Drawing.Size(133, 17); - this.chSync.TabIndex = 0; - this.chSync.Text = "Enable Temporal Sync"; - this.chSync.UseVisualStyleBackColor = true; - this.chSync.CheckedChanged += new System.EventHandler(this.chSync_CheckedChanged); // // grBody // @@ -192,6 +266,7 @@ // chSkeletons // this.chSkeletons.AutoSize = true; + this.chSkeletons.Enabled = false; this.chSkeletons.Location = new System.Drawing.Point(11, 41); this.chSkeletons.Name = "chSkeletons"; this.chSkeletons.Size = new System.Drawing.Size(154, 17); @@ -203,6 +278,7 @@ // chBodyData // this.chBodyData.AutoSize = true; + this.chBodyData.Enabled = false; this.chBodyData.Location = new System.Drawing.Point(11, 19); this.chBodyData.Name = "chBodyData"; this.chBodyData.Size = new System.Drawing.Size(113, 17); @@ -560,7 +636,7 @@ this.grServer.Controls.Add(this.txtICPIters); this.grServer.Controls.Add(this.chMerge); this.grServer.Controls.Add(this.lbICPIters); - this.grServer.Location = new System.Drawing.Point(12, 311); + this.grServer.Location = new System.Drawing.Point(12, 426); this.grServer.Name = "grServer"; this.grServer.Size = new System.Drawing.Size(632, 114); this.grServer.TabIndex = 44; @@ -649,32 +725,25 @@ this.lbOuterIters.Size = new System.Drawing.Size(118, 13); this.lbOuterIters.TabIndex = 26; this.lbOuterIters.Text = "Num of refinement iters:"; - // - // label3 - // - this.label3.Location = new System.Drawing.Point(7, 36); - this.label3.Name = "label3"; - this.label3.Size = new System.Drawing.Size(342, 44); - this.label3.TabIndex = 2; - this.label3.Text = "Before activating, make sure that all Kinects are connected to the server and pro" + - "perly connected via the sync cables. The first device in the server list will be" + - "come the master!"; // // SettingsForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(656, 431); + this.ClientSize = new System.Drawing.Size(656, 554); this.Controls.Add(this.grServer); this.Controls.Add(this.grClient); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; this.MaximizeBox = false; this.Name = "SettingsForm"; this.Text = "Settings"; + this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.SettingsForm_FormClosed); this.Load += new System.EventHandler(this.SettingsForm_Load); this.grClient.ResumeLayout(false); - this.grSync.ResumeLayout(false); - this.grSync.PerformLayout(); + this.grExposure.ResumeLayout(false); + this.grExposure.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.trManualExposure)).EndInit(); + this.grTempSync.ResumeLayout(false); this.grBody.ResumeLayout(false); this.grBody.PerformLayout(); this.grMarkers.ResumeLayout(false); @@ -742,9 +811,14 @@ private System.Windows.Forms.CheckBox chSkeletons; private System.Windows.Forms.ComboBox cbCompressionLevel; private System.Windows.Forms.Label label2; - private System.Windows.Forms.GroupBox grSync; - private System.Windows.Forms.CheckBox chSync; + private System.Windows.Forms.GroupBox grTempSync; private System.Windows.Forms.Label label1; - private System.Windows.Forms.Label label3; + private System.Windows.Forms.Label lbTempSync; + private System.Windows.Forms.Button btSyncDisable; + private System.Windows.Forms.Button btSyncEnable; + private System.Windows.Forms.TrackBar trManualExposure; + private System.Windows.Forms.GroupBox grExposure; + private System.Windows.Forms.Label lbManualExposure; + private System.Windows.Forms.CheckBox chAutoExposureEnabled; } } \ No newline at end of file diff --git a/LiveScanServer/SettingsForm.cs b/LiveScanServer/SettingsForm.cs index b7a0665..0158fe0 100644 --- a/LiveScanServer/SettingsForm.cs +++ b/LiveScanServer/SettingsForm.cs @@ -30,6 +30,8 @@ namespace KinectServer public KinectSettings oSettings; public KinectServer oServer; + private Timer scrollTimer = null; + bool bFormLoaded = false; public SettingsForm() @@ -62,7 +64,12 @@ namespace KinectServer txtICPIters.Text = oSettings.nNumICPIterations.ToString(); txtRefinIters.Text = oSettings.nNumRefineIters.ToString(); - chSync.Checked = oSettings.bEnableSync; + btSyncEnable.Enabled = true; + btSyncDisable.Enabled = false; + + chAutoExposureEnabled.Checked = oSettings.bAutoExposureEnabled; + + trManualExposure.Value = oSettings.nExposureStep; if (oSettings.bSaveAsBinaryPLY) { @@ -83,7 +90,6 @@ namespace KinectServer if (bFormLoaded) { oServer.SendSettings(); - oServer.SendTemporalSyncData(); } } @@ -348,21 +354,105 @@ namespace KinectServer UpdateClients(); } - - private void chSync_CheckedChanged(object sender, EventArgs e) + private void btSyncEnable_click(object sender, EventArgs e) { - oSettings.bEnableSync = chSync.Checked; + if (oServer.GetAllDevicesInitialized() && oServer.nClientCount > 1) + { + oServer.RequestDeviceSyncState(); + btSyncEnable.Enabled = false; + btSyncDisable.Enabled = true; + + //Disable the Auto Exposure, as this could interfere with the temporal sync + chAutoExposureEnabled.Enabled = false; + trManualExposure.Enabled = true; + chAutoExposureEnabled.CheckState = CheckState.Unchecked; + } + + } + + private void btSyncDisable_click(object sender, EventArgs e) + { + if (oServer.GetAllDevicesInitialized()) + { + oServer.DisableTemporalSync(); + btSyncEnable.Enabled = true; + btSyncDisable.Enabled = false; + chAutoExposureEnabled.Enabled = true; + } + } + + public void ActivateTempSyncEnableButton() + { + //As a Form code is run on a single UI thread, we need to communicate with that thread via Invoke + this.BeginInvoke(new MethodInvoker(delegate + { + btSyncEnable.Enabled = true; + btSyncDisable.Enabled = false; + chAutoExposureEnabled.Enabled = true; + })); + } + + private void chAutoExposureEnabled_CheckedChanged(object sender, EventArgs e) + { + oSettings.bAutoExposureEnabled = chAutoExposureEnabled.Checked; UpdateClients(); + + trManualExposure.Enabled = !oSettings.bAutoExposureEnabled; } - private void grSync_Enter(object sender, EventArgs e) - { + /// + /// When the user scrolls on the trackbar, we wait a short amount of time to check if the user has scrolled again. + /// This prevents the Manual Exposure to be set too often, and only sets it when the user has stopped scrolling. + /// Code taken from: https://stackoverflow.com/a/15687418 + /// + /// + /// + private void trManualExposure_Scroll(object sender, EventArgs e) + { + if (scrollTimer == null) + { + // Will tick every 500ms + scrollTimer = new Timer() + { + Enabled = false, + Interval = 300, + Tag = (sender as TrackBar).Value + }; + + scrollTimer.Tick += (s, ea) => + { + // check to see if the value has changed since we last ticked + if (trManualExposure.Value == (int)scrollTimer.Tag) + { + // scrolling has stopped so we are good to go ahead and do stuff + scrollTimer.Stop(); + + // Send the changed exposure to the devices + + //Clamp Exposure Step between -11 and 1 + int exposureStep = trManualExposure.Value; + int exposureStepClamped = exposureStep < -11 ? -11 : exposureStep > 1 ? 1 : exposureStep; + oSettings.nExposureStep = exposureStepClamped; + UpdateClients(); + + + scrollTimer.Dispose(); + scrollTimer = null; + } + else + { + // record the last value seen + scrollTimer.Tag = trManualExposure.Value; + } + }; + scrollTimer.Start(); + } } - private void lbSyncWarning_Click(object sender, EventArgs e) + private void SettingsForm_FormClosed(object sender, FormClosedEventArgs e) { - + oServer.SetSettingsForm(null); } } -} +} \ No newline at end of file diff --git a/include/LiveScanClient/azureKinectCapture.h b/include/LiveScanClient/azureKinectCapture.h index b1ccda3..1a99141 100644 --- a/include/LiveScanClient/azureKinectCapture.h +++ b/include/LiveScanClient/azureKinectCapture.h @@ -3,7 +3,9 @@ #include "stdafx.h" #include "ICapture.h" #include -#include "utils.h" +#include +using namespace std; +//#include "utils.h" class AzureKinectCapture : public ICapture { @@ -11,15 +13,17 @@ public: AzureKinectCapture(); ~AzureKinectCapture(); - bool Initialize(bool asMaster, bool asSubOrdinate, int syncOffset); - bool Close(); + bool Initialize(SYNC_STATE state, int syncOffset); bool AcquireFrame(); + bool Close(); void MapDepthFrameToCameraSpace(Point3f *pCameraSpacePoints); void MapColorFrameToCameraSpace(Point3f *pCameraSpacePoints); void MapDepthFrameToColorSpace(UINT16 *pDepthInColorSpace); void MapColorFrameToDepthSpace(RGB *pColorInDepthSpace); int GetSyncJackState(); uint64_t GetTimeStamp(); + int GetDeviceIndex(); + void SetExposureState(bool enableAutoExposure, int exposureStep); private: @@ -34,6 +38,12 @@ private: k4a_transformation_t transformation = NULL; bool syncInConnected = false; bool syncOutConnected = false; - uint64_t currentTimeStamp; + uint64_t currentTimeStamp = 0; + SYNC_STATE syncState = Standalone; + int deviceIDForRestart = -1; + int restartAttempts = 0; + bool autoExposureEnabled = true; + int exposureTimeStep = 0; void UpdateDepthPointCloud(); + }; \ No newline at end of file diff --git a/include/LiveScanClient/frameFileWriterReader.h b/include/LiveScanClient/frameFileWriterReader.h index 5f80668..b5f7a3f 100644 --- a/include/LiveScanClient/frameFileWriterReader.h +++ b/include/LiveScanClient/frameFileWriterReader.h @@ -10,13 +10,13 @@ class FrameFileWriterReader { public: FrameFileWriterReader(); - void openNewFileForWriting(); + void openNewFileForWriting(int deviceID); void openCurrentFileForReading(); // leave filename blank if you want the filename to be generated from the date void setCurrentFilename(std::string filename = ""); - void writeFrame(std::vector points, std::vector colors, uint64_t timestamp); + void writeFrame(std::vector points, std::vector colors, uint64_t timestamp, int deviceID); bool readFrame(std::vector &outPoints, std::vector &outColors); bool openedForWriting() { return m_bFileOpenedForWriting; } diff --git a/include/LiveScanClient/iCapture.h b/include/LiveScanClient/iCapture.h index a0bf2e8..afe0ca5 100644 --- a/include/LiveScanClient/iCapture.h +++ b/include/LiveScanClient/iCapture.h @@ -40,7 +40,7 @@ public: ICapture(); ~ICapture(); - virtual bool Initialize(bool asMaster, bool asSubOrdinate, int syncOffset) = 0; + virtual bool Initialize(SYNC_STATE state, int syncOffset) = 0; virtual bool AcquireFrame() = 0; virtual bool Close() = 0; virtual void MapDepthFrameToCameraSpace(Point3f *pCameraSpacePoints) = 0; @@ -49,6 +49,9 @@ public: virtual void MapColorFrameToDepthSpace(RGB *pDepthSpacePoints) = 0; virtual int GetSyncJackState() = 0; virtual uint64_t GetTimeStamp() = 0; + virtual int GetDeviceIndex() = 0; + virtual void SetExposureState(bool enableAutoExposure, int exposureStep) = 0; + bool bInitialized; diff --git a/include/LiveScanClient/liveScanClient.h b/include/LiveScanClient/liveScanClient.h index 41f550f..99be1f3 100644 --- a/include/LiveScanClient/liveScanClient.h +++ b/include/LiveScanClient/liveScanClient.h @@ -18,7 +18,7 @@ #include "ImageRenderer.h" #include "SocketCS.h" #include "calibration.h" -#include "utils.h" +//#include "utils.h" #include "azureKinectCapture.h" #include "frameFileWriterReader.h" #include @@ -45,6 +45,7 @@ private: bool m_bIsMaster; bool m_bIsSubOrdinate; + bool m_bRestartingCamera; ICapture *pCapture; @@ -56,10 +57,13 @@ private: bool m_bConfirmCaptured; bool m_bConfirmTempSyncState; bool m_bConfirmSubOrdinateStarted; + bool m_bConfirmRestartAsMaster; bool m_bConfirmCalibrated; bool m_bShowDepth; bool m_bFrameCompression; int m_iCompressionLevel; + bool m_bAutoExposureEnabled; + int m_nExposureStep; enum tempSyncConfig { MASTER, SUBORDINATE, STANDALONE }; tempSyncConfig currentTempSyncState; diff --git a/include/LiveScanClient/utils.h b/include/LiveScanClient/utils.h index a6eeed9..e7918e7 100644 --- a/include/LiveScanClient/utils.h +++ b/include/LiveScanClient/utils.h @@ -30,7 +30,8 @@ enum INCOMING_MESSAGE_TYPE MSG_CLEAR_STORED_FRAMES, MSG_SET_TEMPSYNC_ON, MSG_SET_TEMPSYNC_OFF, - MSG_START_MASTER + MSG_START_MASTER, + MSG_REQUEST_SYNC_JACK_STATE, }; enum OUTGOING_MESSAGE_TYPE @@ -39,7 +40,16 @@ enum OUTGOING_MESSAGE_TYPE MSG_CONFIRM_CALIBRATED, MSG_STORED_FRAME, MSG_LAST_FRAME, - MSG_CONFIRM_TEMP_SYNC_STATUS + MSG_CONFIRM_TEMP_SYNC_STATUS, + MSG_CONFIRM_MASTER_RESTART, + MSG_SYNC_JACK_STATE +}; + +enum SYNC_STATE +{ + Subordinate, + Master, + Standalone }; typedef struct Point3f diff --git a/src/LiveScanClient/azureKinectCapture.cpp b/src/LiveScanClient/azureKinectCapture.cpp index a69a291..8d7a4d4 100644 --- a/src/LiveScanClient/azureKinectCapture.cpp +++ b/src/LiveScanClient/azureKinectCapture.cpp @@ -24,20 +24,51 @@ AzureKinectCapture::~AzureKinectCapture() /// Initalizes this devices as subordinate /// Should only be set when initializing as Subordinate. Each subordinate should have a unique, ascending value /// -bool AzureKinectCapture::Initialize(bool asMaster, bool asSubordinate, int syncOffsetMultiplier) +bool AzureKinectCapture::Initialize(SYNC_STATE state, int syncOffsetMultiplier) { uint32_t count = k4a_device_get_installed_count(); int deviceIdx = 0; + //We save the deviceId of this Client. + //When the cameras are reinitialized during runtime, we can then gurantee + //that each LiveScan instance uses the same device as before (In case two or more Kinects are connected to the same PC) + //A device ID of -1 means that no Kinects has been successfully initalized yet (only happens when the Client starts) + if (deviceIDForRestart != -1) { + deviceIdx = deviceIDForRestart; + } + kinectSensor = NULL; while (K4A_FAILED(k4a_device_open(deviceIdx, &kinectSensor))) { - deviceIdx++; - if (deviceIdx >= count) + + if (deviceIDForRestart == -1) { - bInitialized = false; - return bInitialized; + deviceIdx++; + if (deviceIdx >= count) + { + bInitialized = false; + return bInitialized; + } } + + + else { + + //Sometimes the cameras fail to reinitialize, so we try to initialize them three times with a slight delay before failing + if (restartAttempts > 2) + { + bInitialized = false; + return bInitialized; + } + + else { + + restartAttempts++; + Sleep(200); + } + + } + } k4a_device_configuration_t config = K4A_DEVICE_CONFIG_INIT_DISABLE_ALL; @@ -47,12 +78,12 @@ bool AzureKinectCapture::Initialize(bool asMaster, bool asSubordinate, int syncO config.depth_mode = K4A_DEPTH_MODE_NFOV_UNBINNED; config.synchronized_images_only = true; - if (asMaster) + if (state == Master) { config.wired_sync_mode = K4A_WIRED_SYNC_MODE_MASTER; } - else if (asSubordinate) + else if (state == Subordinate) { config.wired_sync_mode = K4A_WIRED_SYNC_MODE_SUBORDINATE; //Sets the offset on subordinate devices. Should be a multiple of 160, each subordinate having a different multiplier in ascending order. @@ -75,10 +106,24 @@ bool AzureKinectCapture::Initialize(bool asMaster, bool asSubordinate, int syncO return bInitialized; } + //Workaround for a bug. When the camera starts in manual Exposure mode, the brightness of the RBG image + //is much lower than in auto exposure mode. To prevent this, we first set the camera to auto exposure mode and + //then switch to manual mode again if it has been enabled before + + if (autoExposureEnabled == false) { + + k4a_device_set_color_control(kinectSensor, K4A_COLOR_CONTROL_EXPOSURE_TIME_ABSOLUTE, K4A_COLOR_CONTROL_MODE_AUTO, 0); + + //Give it a second to adjust + Sleep(1000); + + SetExposureState(false, exposureTimeStep); + } + transformation = k4a_transformation_create(&calibration); //If this device is a subordinate, it is expected to start capturing at a later time (When the master has started), so we skip this check - if (!asSubordinate) + if (state != Subordinate) { std::chrono::time_point start = std::chrono::system_clock::now(); bool bTemp; @@ -100,6 +145,9 @@ bool AzureKinectCapture::Initialize(bool asMaster, bool asSubordinate, int syncO serialNumber = std::string(serialNoSize, '\0'); k4a_device_get_serialnum(kinectSensor, (char*)serialNumber.c_str(), &serialNoSize); + deviceIDForRestart = deviceIdx; + restartAttempts = 0; + return bInitialized; } @@ -173,6 +221,41 @@ bool AzureKinectCapture::AcquireFrame() return true; } +/// +/// Enables/Disables Auto Exposure and/or sets the exposure to a step value between -11 and -5 +/// The kinect supports exposure values up to 1, but these are only available in lower FPS modes (5 or 15 FPS) +/// For further information on how this value translates to ms-Values, please take a look +/// at this table: https://github.com/microsoft/Azure-Kinect-Sensor-SDK/blob/develop/src/color/color_priv.h +/// +/// The Exposure Step between -11 and 1 +void AzureKinectCapture::SetExposureState(bool enableAutoExposure, int exposureStep) +{ + if (bInitialized) + { + if (enableAutoExposure) + { + k4a_device_set_color_control(kinectSensor, K4A_COLOR_CONTROL_EXPOSURE_TIME_ABSOLUTE, K4A_COLOR_CONTROL_MODE_AUTO, 0); + + autoExposureEnabled = true; + } + + else + { + //Formula copied from here: https://github.com/microsoft/Azure-Kinect-Sensor-SDK/blob/7cd8683a1a71b8baebef4a3537e6edd8639d1e95/examples/k4arecorder/main.cpp#L333 + float absExposure = (exp2f((float)exposureStep) * 1000000.0f); + //We add 0.5 because C++ always truncates when converting to an integer. + //This ensures that values will always be rounded correctly + float absExposureRoundingMargin = absExposure + 0.5; + int32_t absoluteExposureInt = (int32_t)absExposureRoundingMargin; + + k4a_device_set_color_control(kinectSensor, K4A_COLOR_CONTROL_EXPOSURE_TIME_ABSOLUTE, K4A_COLOR_CONTROL_MODE_MANUAL, absoluteExposureInt); + + autoExposureEnabled = false; + exposureTimeStep = exposureStep; + } + } +} + void AzureKinectCapture::UpdateDepthPointCloud() { if (depthPointCloudImage == NULL) @@ -348,3 +431,8 @@ uint64_t AzureKinectCapture::GetTimeStamp() return currentTimeStamp; } +int AzureKinectCapture::GetDeviceIndex() +{ + return deviceIDForRestart; +} + diff --git a/src/LiveScanClient/frameFileWriterReader.cpp b/src/LiveScanClient/frameFileWriterReader.cpp index c1a4f7f..eb52a13 100644 --- a/src/LiveScanClient/frameFileWriterReader.cpp +++ b/src/LiveScanClient/frameFileWriterReader.cpp @@ -39,14 +39,14 @@ void FrameFileWriterReader::openCurrentFileForReading() m_bFileOpenedForWriting = false; } -void FrameFileWriterReader::openNewFileForWriting() +void FrameFileWriterReader::openNewFileForWriting(int deviceID) { closeFileIfOpened(); char filename[1024]; time_t t = time(0); struct tm * now = localtime(&t); - sprintf(filename, "recording_%04d_%02d_%02d_%02d_%02d.bin", now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min, now->tm_sec); + sprintf(filename, "recording_%01d_%04d_%02d_%02d_%02d_%02d.bin", deviceID, now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min, now->tm_sec); m_sFilename = filename; m_pFileHandle = fopen(filename, "wb"); @@ -86,10 +86,10 @@ bool FrameFileWriterReader::readFrame(std::vector &outPoints, std::vect } -void FrameFileWriterReader::writeFrame(std::vector points, std::vector colors, uint64_t timestamp) +void FrameFileWriterReader::writeFrame(std::vector points, std::vector colors, uint64_t timestamp, int deviceID) { if (!m_bFileOpenedForWriting) - openNewFileForWriting(); + openNewFileForWriting(deviceID); FILE *f = m_pFileHandle; diff --git a/src/LiveScanClient/liveScanClient.cpp b/src/LiveScanClient/liveScanClient.cpp index 19c2c9c..36eb510 100644 --- a/src/LiveScanClient/liveScanClient.cpp +++ b/src/LiveScanClient/liveScanClient.cpp @@ -38,13 +38,13 @@ int APIENTRY wWinMain( } LiveScanClient::LiveScanClient() : - m_hWnd(NULL), - m_nLastCounter(0), - m_nFramesSinceUpdate(0), - m_fFreq(0), - m_nNextStatusTime(0LL), - m_pD2DFactory(NULL), - m_pDrawColor(NULL), + m_hWnd(NULL), + m_nLastCounter(0), + m_nFramesSinceUpdate(0), + m_fFreq(0), + m_nNextStatusTime(0LL), + m_pD2DFactory(NULL), + m_pDrawColor(NULL), m_pDepthRGBX(NULL), m_pCameraSpaceCoordinates(NULL), m_pColorInDepthSpace(NULL), @@ -56,13 +56,17 @@ LiveScanClient::LiveScanClient() : m_bConnected(false), m_bConfirmCaptured(false), m_bConfirmCalibrated(false), + m_bConfirmRestartAsMaster(false), m_bShowDepth(false), m_bSocketThread(true), m_bFrameCompression(true), m_iCompressionLevel(2), m_pClientSocket(NULL), m_nFilterNeighbors(10), - m_fFilterThreshold(0.01f) + m_fFilterThreshold(0.01f), + m_bRestartingCamera(false), + m_bAutoExposureEnabled(true), // Which state the Auto Exposure should be set to + m_nExposureStep(-5) { pCapture = new AzureKinectCapture(); @@ -207,7 +211,7 @@ void LiveScanClient::UpdateFrame() if (m_bCaptureFrame) { uint64_t timeStamp = pCapture->GetTimeStamp(); - m_framesFileWriterReader.writeFrame(m_vLastFrameVertices, m_vLastFrameRGB, timeStamp); + m_framesFileWriterReader.writeFrame(m_vLastFrameVertices, m_vLastFrameRGB, timeStamp, pCapture->GetDeviceIndex()); m_bConfirmCaptured = true; m_bCaptureFrame = false; } @@ -277,7 +281,7 @@ LRESULT CALLBACK LiveScanClient::DlgProc(HWND hWnd, UINT message, WPARAM wParam, D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory); // Get and initialize the default Kinect sensor as standalone - bool res = pCapture->Initialize(false, false, 0); + bool res = pCapture->Initialize(Standalone, 0); if (res) { calibration.LoadCalibration(pCapture->serialNumber); @@ -286,6 +290,8 @@ LRESULT CALLBACK LiveScanClient::DlgProc(HWND hWnd, UINT message, WPARAM wParam, m_pCameraSpaceCoordinates = new Point3f[pCapture->nDepthFrameWidth * pCapture->nDepthFrameHeight]; m_pColorInDepthSpace = new RGB[pCapture->nDepthFrameWidth * pCapture->nDepthFrameHeight]; + + pCapture->SetExposureState(true, 0); } else { @@ -455,30 +461,69 @@ void LiveScanClient::HandleSocket() //Determine if this device is a subordinate, master, or standalone int jackState = pCapture->GetSyncJackState(); + bool res = false; + switch (jackState) { case -1: currentTempSyncState = SUBORDINATE; + //Restart this device as Subordinate, with a unique syncOffset (send by the server) - pCapture->Close(); - pCapture->Initialize(false, true, syncOffset); + m_bRestartingCamera = true; + + + res = pCapture->Close(); + if (!res) { + SetStatusMessage(L"Subordinate device failed to close! Restart Application!", 10000, true); + return; + } + + res = pCapture->Initialize(Subordinate, syncOffset); + if (!res) { + SetStatusMessage(L"Subordinate device failed to reinitialize! Restart Application!", 10000, true); + return; + } //Confirm to the server, that we set this device as subordinate m_bConfirmTempSyncState = true; + m_bRestartingCamera = false; break; case 0: currentTempSyncState = MASTER; + //Only Close this device, as it needs to wait for all subordinates to start, before starting itself - pCapture->Close(); + m_bRestartingCamera = true; + + res = pCapture->Close(); + if (!res) { + SetStatusMessage(L"Master device failed to close! Restart Application!", 10000, true); + return; + } + m_bConfirmTempSyncState = true; break; case 1://Device is Standalone currentTempSyncState = STANDALONE; + //Restart this device as Standalone - pCapture->Close(); - pCapture->Initialize(false, false, 0); + m_bRestartingCamera = true; + + res = pCapture->Close(); + if (!res) { + SetStatusMessage(L"Capture device failed to close! Restart Application!", 10000, true); + return; + } + + res = pCapture->Initialize(Standalone, 0); + + if (!res) { + SetStatusMessage(L"Capture device failed to reinitialize! Restart Application!", 10000, true); + return; + } + m_bConfirmTempSyncState = true; + m_bRestartingCamera = false; break; default: break; @@ -488,16 +533,39 @@ void LiveScanClient::HandleSocket() //Sets this device as Standalone else if (received[i] == MSG_SET_TEMPSYNC_OFF) { currentTempSyncState = STANDALONE; - pCapture->Close(); - pCapture->Initialize(false, false, 0); + m_bRestartingCamera = true; + + bool res; + + res = pCapture->Close(); + if (!res) { + SetStatusMessage(L"Capture device failed to close! Restart Application!", 10000, true); + return; + } + + res = pCapture->Initialize(Standalone, 0); + + if (!res) { + SetStatusMessage(L"Capture device failed to reinitialize! Restart Application!", 10000, true); + return; + } + m_bConfirmTempSyncState = true; + m_bRestartingCamera = false; } //Got confirmation from the server that all subs have started, and we can now start the master else if (received[i] == MSG_START_MASTER) { if (currentTempSyncState == MASTER) { - pCapture->Initialize(true, false, 0); + bool res = pCapture->Initialize(Master, 0); + if (!res) { + SetStatusMessage(L"Master device failed to reinitialize! Restart Application!", 10000, true); + return; + } + + m_bConfirmRestartAsMaster = true; + m_bRestartingCamera = false; } } @@ -562,6 +630,14 @@ void LiveScanClient::HandleSocket() m_bFrameCompression = true; else m_bFrameCompression = false; + + m_bAutoExposureEnabled = (received[i] != 0); + i++; + + m_nExposureStep = *(int*)(received.c_str() + i); + i += sizeof(int); + + pCapture->SetExposureState(m_bAutoExposureEnabled, m_nExposureStep); //so that we do not lose the next character in the stream i--; @@ -615,6 +691,18 @@ void LiveScanClient::HandleSocket() { m_framesFileWriterReader.closeFileIfOpened(); } + + else if (received[i] == MSG_REQUEST_SYNC_JACK_STATE) + { + int size = 3; + char* buffer = new char[size]; + buffer[0] = MSG_SYNC_JACK_STATE; + + buffer[1] = pCapture->GetSyncJackState() + 1; //Gets the current Sync Jack State and adds + 1, as we can't send a negative value + + m_pClientSocket->SendBytes(buffer, size); + m_bConfirmTempSyncState = false; + } } if (m_bConfirmCaptured) @@ -642,6 +730,14 @@ void LiveScanClient::HandleSocket() m_bConfirmTempSyncState = false; } + //Send validation to the server that the Master camera has started recording again + if (m_bConfirmRestartAsMaster) + { + byteToSend = MSG_CONFIRM_MASTER_RESTART; + m_pClientSocket->SendBytes(&byteToSend, 1); + m_bConfirmRestartAsMaster = false; + } + if (m_bConfirmCalibrated) { From 3192da1b108165c3c23de7fdce30f14a3a1f9d1b Mon Sep 17 00:00:00 2001 From: Christopher Remde <39704202+ChristopherRemde@users.noreply.github.com> Date: Mon, 14 Dec 2020 17:23:14 +0100 Subject: [PATCH 5/6] Readme Update Added docs for the "Temporal Sync" and "Exposure Control" Feature --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 752a2ae..1febd24 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,15 @@ To start working with our software you will need a Windows machine with the Kine Both the binary and source distributions contain a manual (in the docs directory) which contains the steps necessary to start working with our software (it won't take more than a couple of minutes to set up). +#### Temporal Synchronziaton (Azure Kinect only) #### +LiveScan3D supports the synchronization feature of the Azure Kinect, which will temporally sync multiple devices to allow for a cleaner capture. +To enable it, connect all devices via cable in a Daisy-chain configuration [(more information here)](https://docs.microsoft.com/en-us/azure/kinect-dk/multi-camera-sync/). Open the settings in the LiveScan server and click the "Enable"-Button found under "Temporal Sync". The devices will now perform a restart and after a few seconds you should be able to see the role of the devices (Master, Subordinate, Slave) in the Client-List of the Server. A error message will appear if the devices are not connected properly, or experience other difficulties. +*Note: While the Sync feature is activated, the camera exposure has to be controlled manually.* + +#### Exposure Control (Azure Kinect only) #### +Livescan supports exposure control for all clients. LiveScan uses auto exposure as a default setting. To enable or disable manual exposure, open the settings on the LiveScan server and tick the "Auto exposure enabled" box. If auto exposure is disabled, you can manually set the exposure with the slider provided below the box. + + ## Where to get help ## If you have any problems feel free to contact us: Marek Kowalski , Jacek Naruniec . We usually answer emails quickly (our timezone is CET). From 6fed026d411c05d328ce28f48a6e1e730410e021 Mon Sep 17 00:00:00 2001 From: Christopher Remde <39704202+ChristopherRemde@users.noreply.github.com> Date: Mon, 14 Dec 2020 17:57:40 +0100 Subject: [PATCH 6/6] Added documentation for Azure Kinect features --- docs/LiveScan 3D Azure Kinect Manual.pdf | Bin 0 -> 181473 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/LiveScan 3D Azure Kinect Manual.pdf diff --git a/docs/LiveScan 3D Azure Kinect Manual.pdf b/docs/LiveScan 3D Azure Kinect Manual.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4bcb174bbe8054970614878cb2ae37b858ef3730 GIT binary patch literal 181473 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 ^$k}xOt~?`RG<>I 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