diff --git a/Application/.vs/config/applicationhost.config b/Application/.vs/config/applicationhost.config
new file mode 100644
index 0000000000..f7669cc6b0
--- /dev/null
+++ b/Application/.vs/config/applicationhost.config
@@ -0,0 +1,1022 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Application/Rulr.sln b/Application/Rulr.sln
index 4496d3bc1d..d8a9ee7242 100644
--- a/Application/Rulr.sln
+++ b/Application/Rulr.sln
@@ -12,6 +12,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Rulr", "Rulr.vcxproj", "{08
{C135C231-F751-482D-9E4C-C29473E3DD98} = {C135C231-F751-482D-9E4C-C29473E3DD98}
{CBFC3D47-1E57-4291-90B6-C344B836B8DA} = {CBFC3D47-1E57-4291-90B6-C344B836B8DA}
{4172965B-8375-4DF7-B213-A242DC92BC90} = {4172965B-8375-4DF7-B213-A242DC92BC90}
+ {85FF8660-9C77-43A1-B8EE-F2B3182A6C52} = {85FF8660-9C77-43A1-B8EE-F2B3182A6C52}
{056CE582-26C0-4041-8B4A-EE2B53E77208} = {056CE582-26C0-4041-8B4A-EE2B53E77208}
{09FAF69E-1054-477B-8CF6-B5349771197C} = {09FAF69E-1054-477B-8CF6-B5349771197C}
{45CE5AA6-9DFD-497A-8CAF-1743A8075F5A} = {45CE5AA6-9DFD-497A-8CAF-1743A8075F5A}
@@ -165,6 +166,9 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Plugin_BrightnessAssignment
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Plugin_Calibrate", "..\Plugin_Calibrate\Plugin_Calibrate.vcxproj", "{C135C231-F751-482D-9E4C-C29473E3DD98}"
+ ProjectSection(ProjectDependencies) = postProject
+ {C400D413-78E4-4F60-B5E2-7965FCAC0EEC} = {C400D413-78E4-4F60-B5E2-7965FCAC0EEC}
+ EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SLS", "SLS", "{B5D96B9A-620B-49D9-8A5D-B30C62FC76E4}"
EndProject
@@ -180,6 +184,13 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Plugin_LSS", "..\Plugin_LSS
{C400D413-78E4-4F60-B5E2-7965FCAC0EEC} = {C400D413-78E4-4F60-B5E2-7965FCAC0EEC}
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Experiments", "Experiments", "{B93C7CAE-7E6C-4B10-80F2-FB823EAF130D}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Plugin_Experiments", "..\Plugin_Experiments\Plugin_Experiments.vcxproj", "{85FF8660-9C77-43A1-B8EE-F2B3182A6C52}"
+ ProjectSection(ProjectDependencies) = postProject
+ {C400D413-78E4-4F60-B5E2-7965FCAC0EEC} = {C400D413-78E4-4F60-B5E2-7965FCAC0EEC}
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -231,9 +242,7 @@ Global
{3E918808-ADEE-4B04-9042-25F795280BAD}.Release|x64.ActiveCfg = Release|x64
{3E918808-ADEE-4B04-9042-25F795280BAD}.Release|x64.Build.0 = Release|x64
{CBFC3D47-1E57-4291-90B6-C344B836B8DA}.Debug|x64.ActiveCfg = Release|x64
- {CBFC3D47-1E57-4291-90B6-C344B836B8DA}.Debug|x64.Build.0 = Release|x64
{CBFC3D47-1E57-4291-90B6-C344B836B8DA}.Release|x64.ActiveCfg = Release|x64
- {CBFC3D47-1E57-4291-90B6-C344B836B8DA}.Release|x64.Build.0 = Release|x64
{5B11C632-4E4E-4F86-914D-61F2F1FA6C0B}.Debug|x64.ActiveCfg = Release|x64
{5B11C632-4E4E-4F86-914D-61F2F1FA6C0B}.Debug|x64.Build.0 = Release|x64
{5B11C632-4E4E-4F86-914D-61F2F1FA6C0B}.Release|x64.ActiveCfg = Release|x64
@@ -247,31 +256,23 @@ Global
{4D3BCFDD-E65D-4247-B303-E0839CAEC6A6}.Release|x64.ActiveCfg = Release|x64
{4D3BCFDD-E65D-4247-B303-E0839CAEC6A6}.Release|x64.Build.0 = Release|x64
{5688EFFC-E727-4032-9CC3-2697085BEC57}.Debug|x64.ActiveCfg = Release|x64
- {5688EFFC-E727-4032-9CC3-2697085BEC57}.Debug|x64.Build.0 = Release|x64
{5688EFFC-E727-4032-9CC3-2697085BEC57}.Release|x64.ActiveCfg = Release|x64
- {5688EFFC-E727-4032-9CC3-2697085BEC57}.Release|x64.Build.0 = Release|x64
{C087079E-2A0E-4B85-A479-C28E2D01A048}.Debug|x64.ActiveCfg = Release|x64
{C087079E-2A0E-4B85-A479-C28E2D01A048}.Debug|x64.Build.0 = Release|x64
{C087079E-2A0E-4B85-A479-C28E2D01A048}.Release|x64.ActiveCfg = Release|x64
{C087079E-2A0E-4B85-A479-C28E2D01A048}.Release|x64.Build.0 = Release|x64
{4172965B-8375-4DF7-B213-A242DC92BC90}.Debug|x64.ActiveCfg = Release|x64
- {4172965B-8375-4DF7-B213-A242DC92BC90}.Debug|x64.Build.0 = Release|x64
{4172965B-8375-4DF7-B213-A242DC92BC90}.Release|x64.ActiveCfg = Release|x64
- {4172965B-8375-4DF7-B213-A242DC92BC90}.Release|x64.Build.0 = Release|x64
{DFE9EE67-8247-43D6-A957-813EFAD6111E}.Debug|x64.ActiveCfg = Release|x64
{DFE9EE67-8247-43D6-A957-813EFAD6111E}.Release|x64.ActiveCfg = Release|x64
{09FAF69E-1054-477B-8CF6-B5349771197C}.Debug|x64.ActiveCfg = Release|x64
- {09FAF69E-1054-477B-8CF6-B5349771197C}.Debug|x64.Build.0 = Release|x64
{09FAF69E-1054-477B-8CF6-B5349771197C}.Release|x64.ActiveCfg = Release|x64
- {09FAF69E-1054-477B-8CF6-B5349771197C}.Release|x64.Build.0 = Release|x64
{C400D413-78E4-4F60-B5E2-7965FCAC0EEC}.Debug|x64.ActiveCfg = Release|x64
{C400D413-78E4-4F60-B5E2-7965FCAC0EEC}.Debug|x64.Build.0 = Release|x64
{C400D413-78E4-4F60-B5E2-7965FCAC0EEC}.Release|x64.ActiveCfg = Release|x64
{C400D413-78E4-4F60-B5E2-7965FCAC0EEC}.Release|x64.Build.0 = Release|x64
{F8877612-5358-423D-980F-28A6DBDF252E}.Debug|x64.ActiveCfg = Release|x64
- {F8877612-5358-423D-980F-28A6DBDF252E}.Debug|x64.Build.0 = Release|x64
{F8877612-5358-423D-980F-28A6DBDF252E}.Release|x64.ActiveCfg = Release|x64
- {F8877612-5358-423D-980F-28A6DBDF252E}.Release|x64.Build.0 = Release|x64
{1977CCBA-FAB0-4037-8E67-018DE0022E7C}.Debug|x64.ActiveCfg = Release|x64
{1977CCBA-FAB0-4037-8E67-018DE0022E7C}.Debug|x64.Build.0 = Release|x64
{1977CCBA-FAB0-4037-8E67-018DE0022E7C}.Release|x64.ActiveCfg = Release|x64
@@ -301,9 +302,7 @@ Global
{3985EE69-9945-40C4-839B-C21A17D5F151}.Release|x64.ActiveCfg = Release|x64
{3985EE69-9945-40C4-839B-C21A17D5F151}.Release|x64.Build.0 = Release|x64
{E91D390E-75A3-4435-8CE9-B14271A8866A}.Debug|x64.ActiveCfg = Release|x64
- {E91D390E-75A3-4435-8CE9-B14271A8866A}.Debug|x64.Build.0 = Release|x64
{E91D390E-75A3-4435-8CE9-B14271A8866A}.Release|x64.ActiveCfg = Release|x64
- {E91D390E-75A3-4435-8CE9-B14271A8866A}.Release|x64.Build.0 = Release|x64
{B6EF2661-4D10-4DAE-B4CF-BD0A92EA864C}.Debug|x64.ActiveCfg = Release|x64
{B6EF2661-4D10-4DAE-B4CF-BD0A92EA864C}.Debug|x64.Build.0 = Release|x64
{B6EF2661-4D10-4DAE-B4CF-BD0A92EA864C}.Release|x64.ActiveCfg = Release|x64
@@ -313,9 +312,7 @@ Global
{82E035B1-8706-4571-9774-53990D679319}.Release|x64.ActiveCfg = Release|x64
{82E035B1-8706-4571-9774-53990D679319}.Release|x64.Build.0 = Release|x64
{CAD38B0E-6390-4999-B51F-E4C130D21A30}.Debug|x64.ActiveCfg = Release|x64
- {CAD38B0E-6390-4999-B51F-E4C130D21A30}.Debug|x64.Build.0 = Release|x64
{CAD38B0E-6390-4999-B51F-E4C130D21A30}.Release|x64.ActiveCfg = Release|x64
- {CAD38B0E-6390-4999-B51F-E4C130D21A30}.Release|x64.Build.0 = Release|x64
{45CE5AA6-9DFD-497A-8CAF-1743A8075F5A}.Debug|x64.ActiveCfg = Release|x64
{45CE5AA6-9DFD-497A-8CAF-1743A8075F5A}.Debug|x64.Build.0 = Release|x64
{45CE5AA6-9DFD-497A-8CAF-1743A8075F5A}.Release|x64.ActiveCfg = Release|x64
@@ -327,29 +324,25 @@ Global
{9BF86707-A2E9-4727-B3E1-7D0A3B4CE3E3}.Debug|x64.ActiveCfg = Release|x64
{9BF86707-A2E9-4727-B3E1-7D0A3B4CE3E3}.Release|x64.ActiveCfg = Release|x64
{056CE582-26C0-4041-8B4A-EE2B53E77208}.Debug|x64.ActiveCfg = Release|x64
- {056CE582-26C0-4041-8B4A-EE2B53E77208}.Debug|x64.Build.0 = Release|x64
{056CE582-26C0-4041-8B4A-EE2B53E77208}.Release|x64.ActiveCfg = Release|x64
- {056CE582-26C0-4041-8B4A-EE2B53E77208}.Release|x64.Build.0 = Release|x64
{5C557280-E9E7-4789-83F6-7185A89772BD}.Debug|x64.ActiveCfg = Release|x64
{5C557280-E9E7-4789-83F6-7185A89772BD}.Debug|x64.Build.0 = Release|x64
{5C557280-E9E7-4789-83F6-7185A89772BD}.Release|x64.ActiveCfg = Release|x64
{5C557280-E9E7-4789-83F6-7185A89772BD}.Release|x64.Build.0 = Release|x64
- {5C78C8F2-6D2D-4B46-9D92-E29015F0E66A}.Debug|x64.ActiveCfg = Debug|x64
- {5C78C8F2-6D2D-4B46-9D92-E29015F0E66A}.Debug|x64.Build.0 = Debug|x64
+ {5C78C8F2-6D2D-4B46-9D92-E29015F0E66A}.Debug|x64.ActiveCfg = Release|x64
{5C78C8F2-6D2D-4B46-9D92-E29015F0E66A}.Release|x64.ActiveCfg = Release|x64
- {5C78C8F2-6D2D-4B46-9D92-E29015F0E66A}.Release|x64.Build.0 = Release|x64
- {C135C231-F751-482D-9E4C-C29473E3DD98}.Debug|x64.ActiveCfg = Debug|x64
- {C135C231-F751-482D-9E4C-C29473E3DD98}.Debug|x64.Build.0 = Debug|x64
+ {C135C231-F751-482D-9E4C-C29473E3DD98}.Debug|x64.ActiveCfg = Release|x64
+ {C135C231-F751-482D-9E4C-C29473E3DD98}.Debug|x64.Build.0 = Release|x64
{C135C231-F751-482D-9E4C-C29473E3DD98}.Release|x64.ActiveCfg = Release|x64
{C135C231-F751-482D-9E4C-C29473E3DD98}.Release|x64.Build.0 = Release|x64
- {651FDD8B-6C0C-4A82-89ED-182CFF129D03}.Debug|x64.ActiveCfg = Debug|x64
- {651FDD8B-6C0C-4A82-89ED-182CFF129D03}.Debug|x64.Build.0 = Debug|x64
+ {651FDD8B-6C0C-4A82-89ED-182CFF129D03}.Debug|x64.ActiveCfg = Release|x64
{651FDD8B-6C0C-4A82-89ED-182CFF129D03}.Release|x64.ActiveCfg = Release|x64
- {651FDD8B-6C0C-4A82-89ED-182CFF129D03}.Release|x64.Build.0 = Release|x64
- {E26FB7D4-3D82-413D-88F7-8A1B6EE91A01}.Debug|x64.ActiveCfg = Debug|x64
- {E26FB7D4-3D82-413D-88F7-8A1B6EE91A01}.Debug|x64.Build.0 = Debug|x64
+ {E26FB7D4-3D82-413D-88F7-8A1B6EE91A01}.Debug|x64.ActiveCfg = Release|x64
{E26FB7D4-3D82-413D-88F7-8A1B6EE91A01}.Release|x64.ActiveCfg = Release|x64
- {E26FB7D4-3D82-413D-88F7-8A1B6EE91A01}.Release|x64.Build.0 = Release|x64
+ {85FF8660-9C77-43A1-B8EE-F2B3182A6C52}.Debug|x64.ActiveCfg = Release|x64
+ {85FF8660-9C77-43A1-B8EE-F2B3182A6C52}.Debug|x64.Build.0 = Release|x64
+ {85FF8660-9C77-43A1-B8EE-F2B3182A6C52}.Release|x64.ActiveCfg = Release|x64
+ {85FF8660-9C77-43A1-B8EE-F2B3182A6C52}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -414,5 +407,7 @@ Global
{651FDD8B-6C0C-4A82-89ED-182CFF129D03} = {B5D96B9A-620B-49D9-8A5D-B30C62FC76E4}
{61A0166A-57BD-43D7-BF73-A42CB13A9F1C} = {148566DA-E924-4085-B479-ACA0D1601C9A}
{E26FB7D4-3D82-413D-88F7-8A1B6EE91A01} = {61A0166A-57BD-43D7-BF73-A42CB13A9F1C}
+ {B93C7CAE-7E6C-4B10-80F2-FB823EAF130D} = {148566DA-E924-4085-B479-ACA0D1601C9A}
+ {85FF8660-9C77-43A1-B8EE-F2B3182A6C52} = {B93C7CAE-7E6C-4B10-80F2-FB823EAF130D}
EndGlobalSection
EndGlobal
diff --git a/Core/src/ofxRulr.h b/Core/src/ofxRulr.h
index 2d87232cb4..91d3fd4b1f 100644
--- a/Core/src/ofxRulr.h
+++ b/Core/src/ofxRulr.h
@@ -4,7 +4,6 @@
#include "ofxRulr/Version.h"
#include "ofxRulr/Nodes/Base.h"
-#include "ofxRulr/Nodes/Procedure/Base.h"
#include "ofxRulr/Graph/World.h"
#include "ofxRulr/Graph/FactoryRegister.h"
diff --git a/Core/src/ofxRulr/Utils/CaptureSet.cpp b/Core/src/ofxRulr/Utils/CaptureSet.cpp
index 2b2e215743..500b8742e8 100644
--- a/Core/src/ofxRulr/Utils/CaptureSet.cpp
+++ b/Core/src/ofxRulr/Utils/CaptureSet.cpp
@@ -218,8 +218,14 @@ namespace ofxRulr {
//----------
void AbstractCaptureSet::add(shared_ptr capture) {
+ if (find(this->captures.begin(), this->captures.end(), capture) != this->captures.end()) {
+ return;
+ }
+
auto captureWeak = weak_ptr(capture);
capture->onDeletePressed += [captureWeak, this]() {
+ this->listView->clear();
+ this->viewDirty = true;
auto capture = captureWeak.lock();
if (capture) {
this->remove(capture);
@@ -266,9 +272,11 @@ namespace ofxRulr {
//----------
void AbstractCaptureSet::clear() {
+ this->listView->clear();
while (!this->captures.empty()) {
this->remove(* this->captures.begin());
}
+ this->viewDirty = true;
}
//----------
@@ -393,5 +401,10 @@ namespace ofxRulr {
vector> AbstractCaptureSet::getAllCapturesUntyped() const {
return this->captures;
}
+
+ //----------
+ size_t AbstractCaptureSet::size() const {
+ return this->captures.size();
+ }
}
}
\ No newline at end of file
diff --git a/Core/src/ofxRulr/Utils/CaptureSet.h b/Core/src/ofxRulr/Utils/CaptureSet.h
index 4f11291dfb..2e340f8f4d 100644
--- a/Core/src/ofxRulr/Utils/CaptureSet.h
+++ b/Core/src/ofxRulr/Utils/CaptureSet.h
@@ -27,12 +27,12 @@ namespace ofxRulr {
ofxLiquidEvent onDeletePressed;
ofxLiquidEvent onSelectionChanged;
ofParameter timestamp{ "Timestamp", chrono::system_clock::now() };
+ void rebuildDateStrings();
protected:
ofParameter selected{ "Selected", true };
virtual ofxCvGui::ElementPtr getDataDisplay();
void callbackSelectedChanged(bool &);
- void rebuildDateStrings();
string timeString;
string secondString;
string dateString;
@@ -60,6 +60,8 @@ namespace ofxRulr {
ofxLiquidEvent onChange;
ofxLiquidEvent onSelectionChanged;
+
+ size_t size() const;
protected:
virtual bool getIsMultipleSelectionAllowed() = 0;
vector> captures;
diff --git a/Nodes/src/ofxRulr/Nodes/Item/View.cpp b/Nodes/src/ofxRulr/Nodes/Item/View.cpp
index 6a252ff7ef..6d187ef74e 100644
--- a/Nodes/src/ofxRulr/Nodes/Item/View.cpp
+++ b/Nodes/src/ofxRulr/Nodes/Item/View.cpp
@@ -378,8 +378,8 @@ namespace ofxRulr {
//adapted from https://github.com/Itseez/opencv/blob/master/samples/cpp/calibration.cpp#L170
cv::FileStorage fs(result.filePath, cv::FileStorage::WRITE);
- fs << "image_width" << this->getWidth();
- fs << "image_height" << this->getHeight();
+ fs << "image_width" << (int) this->getWidth();
+ fs << "image_height" << (int) this->getHeight();
fs << "camera_matrix" << this->getCameraMatrix();
fs << "distortion_coefficients" << this->getDistortionCoefficients();
diff --git a/Nodes/src/ofxRulr/Nodes/System/VideoOutput.cpp b/Nodes/src/ofxRulr/Nodes/System/VideoOutput.cpp
index e49c684407..a1c962297a 100644
--- a/Nodes/src/ofxRulr/Nodes/System/VideoOutput.cpp
+++ b/Nodes/src/ofxRulr/Nodes/System/VideoOutput.cpp
@@ -26,7 +26,18 @@ namespace ofxRulr {
VideoOutput::Output::Output(int index, GLFWmonitor * monitor) {
this->index = index;
this->monitor = monitor;
- glfwGetMonitorPhysicalSize(monitor, &this->width, &this->height);
+
+ {
+ auto videoMode = glfwGetVideoMode(monitor);
+ if (videoMode != NULL) {
+ this->width = videoMode->width;
+ this->height = videoMode->height;
+ }
+ else {
+ this->width = 0;
+ this->height = 0;
+ }
+ }
this->name = glfwGetMonitorName(monitor);
}
@@ -532,8 +543,14 @@ namespace ofxRulr {
}
ofSetColor(255);
- const auto textBounds = font.getStringBoundingBox(videoOutput.name, 0, 0);
- font.drawString(videoOutput.name, (int)((selectButton->getWidth() - textBounds.width) / 2.0f), (int)((selectButton->getHeight() + textBounds.height) / 2.0f));
+ {
+ stringstream text;
+ text << videoOutput.name << endl;
+ text << videoOutput.width << "x" << videoOutput.height;
+ const auto textBounds = font.getStringBoundingBox(text.str(), 0, 0);
+ font.drawString(text.str(), (int)((selectButton->getWidth() - textBounds.width) / 2.0f), (int)((selectButton->getHeight() + textBounds.height) / 2.0f));
+ }
+
}
ofPopStyle();
};
diff --git a/Nodes/src/ofxRulr/Nodes/Test/ARCube.cpp b/Nodes/src/ofxRulr/Nodes/Test/ARCube.cpp
index d311260228..1e53901dff 100644
--- a/Nodes/src/ofxRulr/Nodes/Test/ARCube.cpp
+++ b/Nodes/src/ofxRulr/Nodes/Test/ARCube.cpp
@@ -155,8 +155,8 @@ namespace ofxRulr {
}
RULR_CATCH_ALL_TO_ERROR
- //update the fbo
- this->fbo.begin();
+ //update the fbo
+ this->fbo.begin();
{
//draw the undistorted image
this->undistorted.draw(0, 0);
diff --git a/PlatformExamples/VVVV/SplitSymbolNames/main.v4p b/PlatformExamples/VVVV/SplitSymbolNames/main.v4p
index 6768beac9e..76657b3b66 100644
--- a/PlatformExamples/VVVV/SplitSymbolNames/main.v4p
+++ b/PlatformExamples/VVVV/SplitSymbolNames/main.v4p
@@ -1,5 +1,5 @@
-
+
@@ -7,7 +7,7 @@
-
+
@@ -470,4 +470,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugin_ArUco/Plugin_ArUco.vcxproj b/Plugin_ArUco/Plugin_ArUco.vcxproj
index 153bfc7069..78f43357ac 100644
--- a/Plugin_ArUco/Plugin_ArUco.vcxproj
+++ b/Plugin_ArUco/Plugin_ArUco.vcxproj
@@ -89,6 +89,7 @@
+
diff --git a/Plugin_ArUco/Plugin_ArUco.vcxproj.filters b/Plugin_ArUco/Plugin_ArUco.vcxproj.filters
index ad29386b3f..f8a204c52b 100644
--- a/Plugin_ArUco/Plugin_ArUco.vcxproj.filters
+++ b/Plugin_ArUco/Plugin_ArUco.vcxproj.filters
@@ -74,5 +74,8 @@
src\ofxRulr\Nodes\ArUco
+
+ src
+
\ No newline at end of file
diff --git a/Plugin_ArUco/src/Constants_Plugin_ArUco.h b/Plugin_ArUco/src/Constants_Plugin_ArUco.h
new file mode 100644
index 0000000000..75fd3217cd
--- /dev/null
+++ b/Plugin_ArUco/src/Constants_Plugin_ArUco.h
@@ -0,0 +1,7 @@
+#pragma once
+
+#ifdef BUILD_Plugin_ArUco
+#define PLUGIN_ARUCO_EXPORTS __declspec(dllexport)
+#else
+#define PLUGIN_ARUCO_EXPORTS
+#endif
\ No newline at end of file
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/AlignMarkerMap.h b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/AlignMarkerMap.h
index e5c403a996..51d80e2013 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/AlignMarkerMap.h
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/AlignMarkerMap.h
@@ -1,5 +1,6 @@
#pragma once
+#include "Constants_Plugin_ArUco.h"
#include "ofxRulr/Nodes/Base.h"
#include "ofxRulr/Utils/CaptureSet.h"
#include "ofxNonLinearFit.h"
@@ -10,7 +11,7 @@
namespace ofxRulr {
namespace Nodes {
namespace ArUco {
- class AlignMarkerMap : public Nodes::Base {
+ class PLUGIN_ARUCO_EXPORTS AlignMarkerMap : public Nodes::Base {
public:
enum class Plane : int {
X, Y, Z
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/ChArUcoBoard.cpp b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/ChArUcoBoard.cpp
index ffe923f5a7..93af51e6ca 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/ChArUcoBoard.cpp
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/ChArUcoBoard.cpp
@@ -146,6 +146,8 @@ namespace ofxRulr {
detectorParams->adaptiveThreshWinSizeMin = 3;
detectorParams->adaptiveThreshWinSizeMax = 33;
detectorParams->adaptiveThreshWinSizeStep = 5;
+ detectorParams->errorCorrectionRate = this->parameters.detection.errorCorrectionRate;
+
if (image.cols > 5000) {
detectorParams->minMarkerPerimeterRate = 0.003;
}
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/ChArUcoBoard.h b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/ChArUcoBoard.h
index 1d53e6bb3d..b28e11be6a 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/ChArUcoBoard.h
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/ChArUcoBoard.h
@@ -1,5 +1,6 @@
#pragma once
+#include "Constants_Plugin_ArUco.h"
#include "ofxRulr/Nodes/Item/AbstractBoard.h"
#include
#include
@@ -7,7 +8,7 @@
namespace ofxRulr {
namespace Nodes {
namespace ArUco {
- class ChArUcoBoard : public Nodes::Item::AbstractBoard {
+ class PLUGIN_ARUCO_EXPORTS ChArUcoBoard : public Nodes::Item::AbstractBoard {
public:
struct PaperSize {
string name;
@@ -34,13 +35,13 @@ namespace ofxRulr {
struct : ofParameterGroup {
struct : ofParameterGroup {
- ofParameter width{ "Width", 12 };
+ ofParameter width{ "Width", 11 };
ofParameter height{ "Height", 8 };
PARAM_DECLARE("Size [squares]", width, height);
} size;
struct : ofParameterGroup {
- ofParameter square{ "Square", 0.034875, 0.001, 0.10 }; // noah's board
+ ofParameter square{ "Square", 0.025, 0.001, 0.10 };
ofParameter marker{ "Marker", 0.0175, 0.001, 0.1 };
PARAM_DECLARE("Length [m]", square, marker);
} length;
@@ -48,7 +49,8 @@ namespace ofxRulr {
struct : ofParameterGroup {
ofParameter refineStrategy{ "Refine strategy", true };
- PARAM_DECLARE("Detection", refineStrategy);
+ ofParameter errorCorrectionRate{ "Error correction rate", 0.6 };
+ PARAM_DECLARE("Detection", refineStrategy, errorCorrectionRate);
} detection;
struct : ofParameterGroup {
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/Detector.h b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/Detector.h
index e500fe77cc..7a68e3ee5a 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/Detector.h
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/Detector.h
@@ -1,5 +1,6 @@
#pragma once
+#include "Constants_Plugin_ArUco.h"
#include "ofxRulr/Nodes/Base.h"
#include "ofxRulr/Nodes/Item/Camera.h"
#include
@@ -9,7 +10,7 @@
namespace ofxRulr {
namespace Nodes {
namespace ArUco {
- class Detector : public Nodes::Base {
+ class PLUGIN_ARUCO_EXPORTS Detector : public Nodes::Base {
public:
Detector();
string getTypeName() const override;
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/Dictionary.h b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/Dictionary.h
index f0e6891916..abad868af9 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/Dictionary.h
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/Dictionary.h
@@ -1,12 +1,13 @@
#pragma once
+#include "Constants_Plugin_ArUco.h"
#include "ofxRulr/Nodes/Base.h"
#include
namespace ofxRulr {
namespace Nodes {
namespace ArUco {
- class Dictionary : public Nodes::Base {
+ class PLUGIN_ARUCO_EXPORTS Dictionary : public Nodes::Base {
public:
Dictionary();
string getTypeName() const override;
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/FindMarkers.h b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/FindMarkers.h
index 3694dacee3..75de81e700 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/FindMarkers.h
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/FindMarkers.h
@@ -1,5 +1,6 @@
#pragma once
+#include "Constants_Plugin_ArUco.h"
#include "ofxRulr/Nodes/Base.h"
#include
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMap.cpp b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMap.cpp
index efc94354b7..f9a25ea24c 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMap.cpp
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMap.cpp
@@ -65,6 +65,8 @@ namespace ofxRulr {
markerPreview.draw();
}
+ ofDrawBitmapString(ofToString(marker3D.id), ofxCv::toOf(marker3D.points[0]));
+
switch (this->parameters.drawLabels.get().get()) {
case WhenDrawOnWorldStage::Selected:
if (!this->isBeingInspected()) {
@@ -101,11 +103,31 @@ namespace ofxRulr {
inspector->addButton("Clear", [this]() {
this->clear();
});
+
+ {
+ inspector->addTitle("Rotate 90", ofxCvGui::Widgets::Title::Level::H3);
+ inspector->addButton("X", [this]() {
+ this->rotateMarkerMap(ofVec3f(1, 0, 0), 90);
+ });
+ inspector->addButton("Y", [this]() {
+ this->rotateMarkerMap(ofVec3f(0, 1, 0), 90);
+ });
+ inspector->addButton("Z", [this]() {
+ this->rotateMarkerMap(ofVec3f(0, 0, 1), 90);
+ });
+ inspector->addButton("Remove...", [this]() {
+ auto removeString = ofSystemTextBoxDialog("Marker index");
+ if (!removeString.empty()) {
+ auto index = ofToInt(removeString);
+ this->removeMarker(index);
+ }
+ });
+ }
}
//----------
void MarkerMap::serialize(Json::Value & json) {
- if(this->markerMap) {
+ if (this->markerMap) {
auto & jsonMarkerMap = json["markerMap"];
jsonMarkerMap["mInfoType"] = this->markerMap->mInfoType;
jsonMarkerMap["dictionary"] = this->markerMap->getDictionary();
@@ -160,6 +182,30 @@ namespace ofxRulr {
void MarkerMap::clear() {
this->markerMap.reset();
}
+
+ //----------
+ void MarkerMap::rotateMarkerMap(const ofVec3f & axis, float angle) {
+ if (this->markerMap) {
+ auto transform = ofMatrix4x4::newRotationMatrix(angle, axis);
+ for (auto & marker : *this->markerMap) {
+ for (auto & point : marker.points) {
+ point = ofxCv::toCv(ofxCv::toOf(point) * transform);
+ }
+ }
+ }
+ }
+
+ //----------
+ void MarkerMap::removeMarker(size_t idToRemove) {
+ if (this->markerMap) {
+ auto toRemove = remove_if(this->markerMap->begin()
+ , this->markerMap->end()
+ , [idToRemove](const aruco::Marker3DInfo & markerInfo) {
+ return markerInfo.id == idToRemove;
+ });
+ this->markerMap->erase(toRemove, this->markerMap->end());
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMap.h b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMap.h
index b5d16249be..489c869bb5 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMap.h
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMap.h
@@ -1,12 +1,13 @@
#pragma once
+#include "Constants_Plugin_ArUco.h"
#include "ofxRulr/Nodes/Base.h"
#include
namespace ofxRulr {
namespace Nodes {
namespace ArUco {
- class MarkerMap : public Nodes::Base {
+ class PLUGIN_ARUCO_EXPORTS MarkerMap : public Nodes::Base {
public:
MarkerMap();
string getTypeName() const override;
@@ -20,6 +21,8 @@ namespace ofxRulr {
shared_ptr getMarkerMap();
void clear();
+ void rotateMarkerMap(const ofVec3f & axis, float angle);
+ void removeMarker(size_t idToRemove);
protected:
shared_ptr markerMap;
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMapPoseTracker.cpp b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMapPoseTracker.cpp
index c96d171cd4..beae07d25e 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMapPoseTracker.cpp
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMapPoseTracker.cpp
@@ -130,7 +130,8 @@ namespace ofxRulr {
, cameraNode->getDistortionCoefficients()
, rotation
, translation
- , true);
+ , true
+ , cv::SOLVEPNP_ITERATIVE);
}
else {
cv::solvePnPRansac(worldPoints
@@ -139,7 +140,8 @@ namespace ofxRulr {
, cameraNode->getDistortionCoefficients()
, rotation
, translation
- , true);
+ , true
+ , cv::SOLVEPNP_ITERATIVE);
}
break;
}
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMapPoseTracker.h b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMapPoseTracker.h
index 61e35cc5b2..e1334b9198 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMapPoseTracker.h
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/MarkerMapPoseTracker.h
@@ -1,12 +1,13 @@
#pragma once
+#include "Constants_Plugin_ArUco.h"
#include "ofxRulr/Nodes/Base.h"
#include
namespace ofxRulr {
namespace Nodes {
namespace ArUco {
- class MarkerMapPoseTracker : public Nodes::Base {
+ class PLUGIN_ARUCO_EXPORTS MarkerMapPoseTracker : public Nodes::Base {
public:
MAKE_ENUM(Method
, (Tracker, solvePnP, RANSAC)
diff --git a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/OSCRelay.h b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/OSCRelay.h
index 8529949e3b..73cb6c7d9f 100644
--- a/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/OSCRelay.h
+++ b/Plugin_ArUco/src/ofxRulr/Nodes/ArUco/OSCRelay.h
@@ -1,12 +1,13 @@
#pragma once
+#include "Constants_Plugin_ArUco.h"
#include "ofxRulr/Nodes/Base.h"
#include "ofxOsc.h"
namespace ofxRulr {
namespace Nodes {
namespace ArUco {
- class OSCRelay : public ofxRulr::Nodes::Base {
+ class PLUGIN_ARUCO_EXPORTS OSCRelay : public ofxRulr::Nodes::Base {
public:
OSCRelay();
string getTypeName() const override;
diff --git a/Plugin_ArUco/src/pch_Plugin_ArUco.h b/Plugin_ArUco/src/pch_Plugin_ArUco.h
index b035b32ed1..0607f73481 100644
--- a/Plugin_ArUco/src/pch_Plugin_ArUco.h
+++ b/Plugin_ArUco/src/pch_Plugin_ArUco.h
@@ -16,3 +16,5 @@
#include "ofxRulr/Nodes/ArUco/MarkerMap.h"
#include "ofxRulr/Nodes/ArUco/MarkerMapPoseTracker.h"
#include "ofxRulr/Nodes/ArUco/OSCRelay.h"
+
+#include "pch_Plugin_ArUco.h"
diff --git a/Plugin_Calibrate/Plugin_Calibrate.vcxproj b/Plugin_Calibrate/Plugin_Calibrate.vcxproj
index 44d6d305dd..19813f2c4d 100644
--- a/Plugin_Calibrate/Plugin_Calibrate.vcxproj
+++ b/Plugin_Calibrate/Plugin_Calibrate.vcxproj
@@ -12,7 +12,7 @@
{C135C231-F751-482D-9E4C-C29473E3DD98}
- ofxRulr::Nodes::BAM
+ ofxRulr::Nodes::Calibrate
8.1
Plugin_Calibrate
diff --git a/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/CameraIntrinsics.cpp b/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/CameraIntrinsics.cpp
index e528b0256f..8f4f2817f9 100644
--- a/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/CameraIntrinsics.cpp
+++ b/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/CameraIntrinsics.cpp
@@ -110,6 +110,10 @@ namespace ofxRulr {
//draw current corners on top
ofxCv::drawCorners(this->currentImagePoints);
+ for (int i = 0; i < this->currentImagePoints.size(); i++) {
+ ofDrawBitmapString(ofToString(i), this->currentImagePoints[i]);
+ }
+
ofPopStyle();
}
ofPopMatrix();
@@ -238,6 +242,72 @@ namespace ofxRulr {
return false;
}
+ //----------
+ void CameraIntrinsics::addImage(cv::Mat image) {
+ this->throwIfMissingAConnection();
+ this->throwIfMissingAConnection();
+
+ auto board = this->getInput();
+ auto camera = this->getInput();
+
+ vector imagePoints;
+ vector objectPoints;
+
+ board->findBoard(image
+ , toCv(imagePoints)
+ , toCv(objectPoints)
+ , this->parameters.capture.findBoardMode
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients());
+
+ {
+ auto capture = make_shared();
+ capture->pointsImageSpace = imagePoints;
+ capture->pointsObjectSpace = objectPoints;
+ capture->imageWidth = camera->getWidth();
+ capture->imageHeight = camera->getHeight();
+ this->captures.add(capture);
+ }
+ }
+
+ //----------
+ void CameraIntrinsics::addFolder(const std::filesystem::path & path) {
+
+ //get list of files
+ vector filePaths;
+ for (std::filesystem::directory_iterator it(path)
+ ; it != std::filesystem::directory_iterator()
+ ; ++it) {
+ filePaths.push_back(it->path().string());
+ }
+
+ Utils::ScopedProcess scopedProcess("Adding folder images", true, filePaths.size());
+
+ //for each file in path
+ for(auto filePath : filePaths) {
+ try {
+ Utils::ScopedProcess fileScopedProcess(filePath, false);
+
+ //load image
+ auto image = cv::imread(filePath);
+
+ //check it loaded
+ if (image.empty()) {
+ continue;
+ }
+ else {
+ //add it to the captures
+ this->addImage(image);
+ }
+ }
+ RULR_CATCH_ALL_TO({
+ RULR_WARNING << e.what();
+ });
+ }
+
+ scopedProcess.end();
+ }
+
//----------
void CameraIntrinsics::populateInspector(ofxCvGui::InspectArguments & inspectArguments) {
auto inspector = inspectArguments.inspector;
@@ -278,6 +348,15 @@ namespace ofxRulr {
inspector->addSpacer();
+ inspector->addButton("Add folder of images", [this]() {
+ auto result = ofSystemLoadDialog("Folder of calibration images", true);
+ if (result.bSuccess) {
+ this->addFolder(std::filesystem::path(result.getPath()));
+ }
+ });
+
+ inspector->addSpacer();
+
inspector->addParameterGroup(this->parameters);
}
@@ -312,6 +391,7 @@ namespace ofxRulr {
this->captures.add(capture);
}
}
+
//----------
void CameraIntrinsics::findBoard() {
this->throwIfMissingAnyConnection();
@@ -346,18 +426,20 @@ namespace ofxRulr {
void CameraIntrinsics::calibrate() {
this->throwIfMissingAConnection();
- auto allCaptures = this->captures.getAllCaptures();
+ auto captures = this->captures.getSelection();
- if (allCaptures.size() < 2) {
+ if (captures.size() < 2) {
throw(ofxRulr::Exception("You need to add at least 2 captures before trying to calibrate (ideally 10)"));
}
auto camera = this->getInput();
vector> imagePoints;
vector> objectPoints;
- for (auto & capture : allCaptures) {
- imagePoints.push_back(toCv(capture->pointsImageSpace));
- objectPoints.push_back(toCv(capture->pointsObjectSpace));
+ for (auto & capture : captures) {
+ if (capture->pointsImageSpace.size() > 0) {
+ imagePoints.push_back(toCv(capture->pointsImageSpace));
+ objectPoints.push_back(toCv(capture->pointsObjectSpace));
+ }
}
cv::Size cameraResolution(camera->getWidth(), camera->getHeight());
@@ -369,15 +451,16 @@ namespace ofxRulr {
this->error = cv::calibrateCamera(objectPoints, imagePoints, cameraResolution, cameraMatrix, distortionCoefficients, Rotations, Translations, flags);
camera->setIntrinsics(cameraMatrix, distortionCoefficients);
- for (int i = 0; i < allCaptures.size(); i++) {
- auto capture = allCaptures[i];
+ for (int i = 0; i < captures.size(); i++) {
+ auto capture = captures[i];
capture->extrsinsics = makeMatrix(Rotations[i], Translations[i]);
vector reprojectedImageCoordinates;
cv::projectPoints(toCv(capture->pointsObjectSpace), Rotations[i], Translations[i], cameraMatrix, distortionCoefficients, reprojectedImageCoordinates);
float reprojectionErrorSquaredSum = 0.0f;
- for (int i = 0; i < reprojectedImageCoordinates.size(); i++) {
- reprojectionErrorSquaredSum += (toOf(reprojectedImageCoordinates[i]) - capture->pointsImageSpace[i]).lengthSquared();
+
+ for (int j = 0; j < reprojectedImageCoordinates.size(); j++) {
+ reprojectionErrorSquaredSum += (toOf(reprojectedImageCoordinates[j]) - capture->pointsImageSpace[j]).lengthSquared();
}
capture->reprojectionError = sqrt(reprojectionErrorSquaredSum / (float)reprojectedImageCoordinates.size());
}
diff --git a/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/CameraIntrinsics.h b/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/CameraIntrinsics.h
index 0a5e6d1dfe..df277a641f 100644
--- a/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/CameraIntrinsics.h
+++ b/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/CameraIntrinsics.h
@@ -44,6 +44,12 @@ namespace ofxRulr {
void deserialize(const Json::Value &);
bool getRunFinderEnabled() const;
+
+ // Manually add an image to the capture set
+ void addImage(cv::Mat);
+
+ // Add a folder of images to the capture set
+ void addFolder(const std::filesystem::path & path);
protected:
void populateInspector(ofxCvGui::InspectArguments &);
void addCapture(bool triggeredFromTetheredCapture);
diff --git a/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/IReferenceVertices.h b/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/IReferenceVertices.h
index ff35a3e08f..e3586f7793 100644
--- a/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/IReferenceVertices.h
+++ b/Plugin_Calibrate/src/ofxRulr/Nodes/Procedure/Calibrate/IReferenceVertices.h
@@ -4,6 +4,7 @@
#include "ofxCvGui/Panels/Image.h"
#include "ofxRulr/Utils/CaptureSet.h"
+#include "ofxRulr/Nodes/Procedure/Base.h"
#include "Constants_Plugin_Calibration.h"
@@ -20,6 +21,7 @@ namespace ofxRulr {
string getDisplayString() const override;
ofParameter worldPosition{ "World", ofVec3f() };
ofParameter viewPosition{ "View", ofVec2f() };
+ ofParameter name{ "Name", "" };
void setOwner(shared_ptr);
protected:
void drawObjectLines();
diff --git a/Plugin_Experiments/Plugin_Experiments.vcxproj b/Plugin_Experiments/Plugin_Experiments.vcxproj
new file mode 100644
index 0000000000..2c30dab5f9
--- /dev/null
+++ b/Plugin_Experiments/Plugin_Experiments.vcxproj
@@ -0,0 +1,126 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ {85FF8660-9C77-43A1-B8EE-F2B3182A6C52}
+ ofxRulr::Nodes::Experiments
+ 8.1
+ Plugin_Experiments
+
+
+
+ DynamicLibrary
+ true
+ v140
+ MultiByte
+
+
+ DynamicLibrary
+ false
+ v140
+ true
+ MultiByte
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Level3
+ Disabled
+ true
+ Use
+ pch_Plugin_Experiments.h
+ ProgramDatabase
+ ../../../addons/ofxRulr/Plugin_ArUco/src;%(AdditionalIncludeDirectories)
+
+
+ true
+
+
+
+
+
+ Level3
+ MaxSpeed
+ true
+ true
+ true
+ Use
+ pch_Plugin_Experiments.h
+ ../../../addons/ofxRulr/Plugin_ArUco/src;%(AdditionalIncludeDirectories)
+
+
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create
+ Create
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {fc33c381-dc7a-45ad-8ef0-647e3b03db69}
+
+
+ {45ce5aa6-9dfd-497a-8caf-1743a8075f5a}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugin_Experiments/Plugin_Experiments.vcxproj.filters b/Plugin_Experiments/Plugin_Experiments.vcxproj.filters
new file mode 100644
index 0000000000..0a08ec4a07
--- /dev/null
+++ b/Plugin_Experiments/Plugin_Experiments.vcxproj.filters
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+ src\ofxRulr\Experiments\SolvePnP
+
+
+ src\ofxRulr\Experiments\SolvePnP
+
+
+ src\ofxRulr\Experiments\SolvePnP
+
+
+ src\ofxRulr\Experiments\SolveMirror
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\PhotoScan
+
+
+
+
+
+ src\ofxRulr\Experiments\SolvePnP
+
+
+ src\ofxRulr\Experiments\SolvePnP
+
+
+ src\ofxRulr\Experiments\SolvePnP
+
+
+ src\ofxRulr\Experiments\SolveMirror
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\MirrorPlaneCapture
+
+
+ src\ofxRulr\Experiments\PhotoScan
+
+
+
+
+ {9913a1d7-50cf-431a-ba37-d925c1efc463}
+
+
+ {8551b9aa-83e9-4543-977a-36e938e411b5}
+
+
+ {fb0eceac-a621-45ff-b656-68b1b3888be2}
+
+
+ {57879dba-d2a0-4222-8555-3d67ad5d06b0}
+
+
+ {a13b624c-1893-4263-8bc5-3d213d810fae}
+
+
+ {6225c5c4-68e7-4fa6-90ab-17ef387eb1a9}
+
+
+ {5b820a98-344a-439f-b8b4-cf8c10af6673}
+
+
+
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardInMirror.cpp b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardInMirror.cpp
new file mode 100644
index 0000000000..747af3cd44
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardInMirror.cpp
@@ -0,0 +1,550 @@
+#include "pch_Plugin_Experiments.h"
+
+template
+T mean(const vector & points) {
+ T accumulator(0);
+
+ for (const auto & point : points) {
+ accumulator += point;
+ }
+ return accumulator / size(points);
+}
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace MirrorPlaneCapture {
+ //----------
+ BoardInMirror::BoardInMirror() {
+ RULR_NODE_INIT_LISTENER;
+ }
+
+ //----------
+ string BoardInMirror::getTypeName() const {
+ return "Experiments::MirrorPlaneCapture::BoardInMirror";
+ }
+
+ //----------
+ void BoardInMirror::init() {
+ RULR_NODE_DRAW_WORLD_LISTENER;
+ RULR_NODE_INSPECTOR_LISTENER;
+ RULR_NODE_UPDATE_LISTENER;
+
+ this->addInput();
+ this->addInput();
+ this->addInput();
+ this->addInput();
+
+ this->panel = make_shared();
+ this->captures.populateWidgets(this->panel);
+
+ this->manageParameters(this->parameters);
+ }
+
+ //----------
+ void BoardInMirror::update() {
+
+ }
+
+ //----------
+ void BoardInMirror::addCapture() {
+ this->throwIfMissingAConnection();
+
+ auto camera = this->getInput();
+ auto grabber = camera->getGrabber();
+ if (!grabber) {
+ throw(ofxRulr::Exception("Camera grabber not available"));
+ }
+
+ auto frame = grabber->getFreshFrame();
+ auto image = ofxCv::toCv(frame->getPixels());
+ this->addImage(image);
+ }
+
+ //----------
+ void BoardInMirror::calibrate() {
+
+ }
+
+ //----------
+ ofxCvGui::PanelPtr BoardInMirror::getPanel() {
+ return this->panel;
+ }
+
+ //----------
+ void BoardInMirror::populateInspector(ofxCvGui::InspectArguments & inspectArgs) {
+ auto inspector = inspectArgs.inspector;
+ inspector->addButton("Add capture", [this]() {
+ try {
+ Utils::ScopedProcess scopedProcess("Add capture");
+ this->addCapture();
+ scopedProcess.end();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ }, ' ')->setHeight(100.0f);
+
+ inspector->addButton("Add folder of images...", [this]() {
+ auto result = ofSystemLoadDialog("Folder of calibration images", true);
+ if (result.bSuccess) {
+ this->addFolderOfImages(std::filesystem::path(result.getPath()));
+ }
+ });
+
+ inspector->addButton("Calibrate", [this]() {
+ try {
+ this->calibrate();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ }, OF_KEY_RETURN)->setHeight(100.0f);
+ }
+
+ //----------
+ void BoardInMirror::drawWorldStage() {
+ auto captures = this->captures.getSelection();
+ for (auto capture : captures) {
+ capture->drawWorld();
+ }
+ }
+
+ //----------
+ void BoardInMirror::addImage(const cv::Mat & image, const string & name) {
+ this->throwIfMissingAConnection();
+ this->throwIfMissingAConnection();
+
+ auto camera = this->getInput();
+ auto board = this->getInput();
+
+ auto capture = make_shared();
+
+ capture->name = name;
+
+ //function to test reprojection error for a set of points given a translation, rotation
+ auto getReprojectionError = [&camera](const vector & objectPoints
+ , const vector & imagePoints
+ , const cv::Mat & rotationVector
+ , const cv::Mat & translation) {
+
+ float error = 0.0f;
+ vector reprojectedPoints;
+ cv::projectPoints(objectPoints
+ , rotationVector
+ , translation
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , reprojectedPoints);
+
+ //Take square mean
+ for (size_t i = 0; i < imagePoints.size(); i++) {
+ auto delta = imagePoints[i] - reprojectedPoints[i];
+ error += delta.x * delta.x + delta.y * delta.y;
+ }
+ //RMS
+ error /= (float)imagePoints.size();
+ error = sqrt(error);
+
+ return error;
+ };
+
+ //resolve pose of camera using the marker map
+ capture->cameraNavigation.enabled = this->parameters.cameraNavigation.enabled;
+ if (this->parameters.cameraNavigation.enabled) {
+ this->throwIfMissingAConnection();
+ this->throwIfMissingAConnection();
+
+ auto detector = this->getInput();
+ auto markerMap = this->getInput()->getMarkerMap();
+
+ auto markers = detector->getMarkerDetector().detect(image);
+ if (markers.size() < this->parameters.cameraNavigation.minimumMarkers) {
+ throw(ofxRulr::Exception("Found " + ofToString(markers.size()) + " real markers. Need " + ofToString(this->parameters.cameraNavigation.minimumMarkers) + " for camera navigation"));
+ }
+
+ //build up dataset
+ vector worldPoints;
+ vector imagePoints;
+ {
+ for (const auto & marker : markers) {
+ try {
+ auto marker3D = markerMap->getMarker3DInfo(marker.id);
+ for (int i = 0; i < 4; i++) {
+ worldPoints.push_back(marker3D.points[i]);
+ imagePoints.push_back(marker[i]);
+ }
+ }
+ RULR_CATCH_ALL_TO_ERROR; //in the case that the marker did not exist
+ }
+ }
+
+ cv::Mat rotationVector, translation;
+ camera->getExtrinsics(rotationVector, translation, true);
+
+ //Solve camera extrinsics
+ {
+ //first pass EPNP
+ cv::solvePnPRansac(worldPoints
+ , imagePoints
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , rotationVector
+ , translation
+ , true
+ , 100
+ , 5.0f
+ , 0.99
+ , cv::noArray()
+ , cv::SOLVEPNP_EPNP);
+
+ //second pass ITERATIVE
+ cv::solvePnPRansac(worldPoints
+ , imagePoints
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , rotationVector
+ , translation
+ , true
+ , 100
+ , 5.0f
+ , 0.99
+ , cv::noArray()
+ , cv::SOLVEPNP_ITERATIVE);
+ }
+
+ camera->setExtrinsics(rotationVector, translation, true);
+
+ capture->cameraNavigation.imagePoints = imagePoints;
+ capture->cameraNavigation.worldPoints = worldPoints;
+ capture->cameraNavigation.reprojectionError = getReprojectionError(worldPoints
+ , imagePoints
+ , rotationVector
+ , translation);
+ capture->cameraNavigation.cameraPosition = camera->getPosition();
+ }
+
+ auto cameraTransform = camera->getTransform();
+
+ //--
+ //find board plane function
+ //
+ //
+ auto findBoardPlane = [this, camera, board, &getReprojectionError, &cameraTransform](const cv::Mat & image, Capture::Plane & plane, bool mirrored) {
+
+ auto spaceName = string(mirrored ? "mirrored" : "real");
+ //flip image if needed
+ cv::Mat image2;
+ if (mirrored) {
+ cv::flip(image, image2, 0);
+ }
+ else {
+ image2 = image;
+ }
+
+ //get board image and object space points
+ if (!board->findBoard(image2
+ , plane.imagePoints
+ , plane.objectPoints
+ , this->parameters.capture.findBoardMode
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients())) {
+ throw(Exception("Failed to find board in " + spaceName + " space"));
+ }
+
+ //check we found enough corners
+ if (plane.objectPoints.size() < this->parameters.capture.minimumCorners) {
+ throw(Exception("Not enough corners found in " + spaceName + " space (" + ofToString(plane.objectPoints.size()) + " found. Needs " + ofToString(this->parameters.capture.minimumCorners) + ")"));
+ }
+
+ //flip result if needed
+ if (mirrored) {
+ //flip the image
+ for (auto & imagePoint : plane.imagePoints) {
+ imagePoint.y = camera->getHeight() - imagePoint.y; // not -1 again because subpixel
+ }
+
+ //flip the object points
+ for (auto & objectPoint : plane.objectPoints) {
+ objectPoint.y *= -1.0f;
+ }
+ }
+
+ cv::Mat rotation, translation;
+ {
+ float acceptableReprojectionError = ofClamp(camera->getWidth() / 1000, 1, 10);
+
+ //first pass EPNP
+ cv::solvePnPRansac(plane.objectPoints
+ , plane.imagePoints
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , rotation
+ , translation
+ , false
+ , 100
+ , acceptableReprojectionError
+ , 0.99
+ , cv::noArray()
+ , cv::SOLVEPNP_EPNP);
+
+ //second pass ITERATIVE
+ cv::solvePnPRansac(plane.objectPoints
+ , plane.imagePoints
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , rotation
+ , translation
+ , true
+ , 100
+ , acceptableReprojectionError
+ , 0.99
+ , cv::noArray()
+ , cv::SOLVEPNP_ITERATIVE);
+ }
+
+ //record reprojection error
+ plane.reprojectionError = getReprojectionError(plane.objectPoints
+ , plane.imagePoints
+ , rotation
+ , translation);
+
+ //build up world space points
+ plane.boardTransform = ofxCv::makeMatrix(rotation, translation) * cameraTransform;
+
+ for (const auto & objectPoint : plane.objectPoints) {
+ plane.worldPoints.push_back(ofxCv::toOf(objectPoint) * plane.boardTransform);
+ }
+
+ if (!plane.plane.fitToPoints(plane.worldPoints, plane.planeFitResidual)) {
+ throw(Exception("Failed to fit board plane to points in " + spaceName + "space"));
+ }
+
+ //take mean of object space points
+ plane.meanObjectPoint = mean(ofxCv::toOf(plane.objectPoints));
+ };
+ //
+ //--
+
+
+
+ //find board plane in real space
+ findBoardPlane(image, capture->realPlane, false);
+
+ //find board plane in virtual space
+ findBoardPlane(image, capture->virtualPlane, true);
+
+ //solve mirror plane
+ {
+ //choose an object point to use
+ capture->testObjectPoint = (capture->realPlane.meanObjectPoint + capture->virtualPlane.meanObjectPoint * ofVec3f(1, -1, 1)) / 2.0f;
+
+ //get the test points in real and virtual planes
+ capture->realPlane.testPointWorld = capture->testObjectPoint * capture->realPlane.boardTransform;
+ capture->virtualPlane.testPointWorld = (capture->testObjectPoint * ofVec3f(1, -1, 1)) * capture->virtualPlane.boardTransform;
+
+ capture->mirrorPlane = ofxRay::Plane();
+ capture->mirrorPlane.setInfinite(true);
+ capture->mirrorPlane.setCenter((capture->realPlane.testPointWorld + capture->virtualPlane.testPointWorld) / 2.0f);
+ capture->mirrorPlane.setNormal((capture->realPlane.testPointWorld - capture->virtualPlane.testPointWorld).getNormalized());
+
+
+ //where is the test point reflected?
+ {
+ const auto view = camera->getViewInWorldSpace();
+
+ //ray to test point in reflection
+ auto rayToReflectionPoint = ofxRay::Ray(camera->getPosition()
+ , ofVec3f()
+ , capture->color
+ , false);
+
+ rayToReflectionPoint.setEnd(capture->virtualPlane.testPointWorld);
+
+ if (!capture->mirrorPlane.intersect(rayToReflectionPoint, capture->estimatedPointOnMirrorPlane)) {
+ throw(new Exception("Ray towards reflected test point does not intersect with mirror plane"));
+ }
+
+ capture->mirrorPlane.setCenter(capture->estimatedPointOnMirrorPlane);
+ }
+
+ // for visualization purposes
+ capture->mirrorPlane.setScale({ 1.0f, 1.0f });
+ capture->mirrorPlane.setUp({ 0.0f, 1.0f, 1.0f });
+ capture->mirrorPlane.setInfinite(false);
+ capture->mirrorPlane.color = capture->color;
+ }
+
+ //log to server
+ if (this->parameters.logToServer.enabled) {
+ try {
+ Utils::ScopedProcess scopedProcessLogToServer("Log to server");
+ Json::Value requestJson;
+
+ {
+ auto & data = requestJson["data"];
+
+ auto & jsonPlane = data["plane"];
+
+ jsonPlane["ABCD"] << capture->mirrorPlane.getABCD();
+ jsonPlane["center"] << capture->mirrorPlane.getCenter();
+ jsonPlane["normal"] << capture->mirrorPlane.getNormal();
+ }
+
+ ofHttpRequest request(this->parameters.logToServer.address.get(), "log");
+ request.method = ofHttpRequest::Method::POST;
+ request.body = requestJson.toStyledString();
+ request.contentType = "application/json";
+
+ ofURLFileLoader urlLoader;
+ auto response = urlLoader.handleRequest(request);
+ cout << response.data;
+ scopedProcessLogToServer.end();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ }
+
+ this->captures.add(capture);
+ }
+
+ //----------
+ void BoardInMirror::addFolderOfImages(const std::filesystem::path & path) {
+ //get list of files
+ vector filePaths;
+ for (std::filesystem::directory_iterator it(path)
+ ; it != std::filesystem::directory_iterator()
+ ; ++it) {
+ filePaths.push_back(it->path().string());
+ }
+
+ Utils::ScopedProcess scopedProcess("Adding folder images", true, filePaths.size());
+
+ //for each file in path
+ for (auto filePath : filePaths) {
+ try {
+ Utils::ScopedProcess fileScopedProcess(filePath, false);
+
+ //load image
+ auto image = cv::imread(filePath);
+
+ //check it loaded
+ if (image.empty()) {
+ continue;
+ }
+ else {
+ //add it to the captures
+ this->addImage(image, ofFilePath::getBaseName(filePath));
+ }
+ }
+ RULR_CATCH_ALL_TO({
+ RULR_WARNING << e.what();
+ });
+ }
+
+ scopedProcess.end();
+ }
+
+#pragma mark Capture
+ //----------
+ void BoardInMirror::Capture::drawWorld() {
+ this->drawPlane(this->realPlane);
+ this->drawPlane(this->virtualPlane);
+
+ ofPushStyle();
+ {
+ ofSetColor(100, 100, 255);
+ ofDrawSphere(this->realPlane.testPointWorld, 0.01);
+
+ ofSetColor(100, 255, 100);
+ ofDrawSphere(this->virtualPlane.testPointWorld, 0.01);
+ }
+ ofPopStyle();
+
+ //draw the mirror
+ ofPushMatrix();
+ {
+ ofQuaternion rotation;
+ rotation.makeRotate(ofVec3f(0, 0, 1), this->mirrorPlane.getNormal());
+
+ ofTranslate(this->mirrorPlane.getCenter());
+ ofMultMatrix(ofMatrix4x4(rotation));
+
+ ofPushStyle();
+ {
+ ofSetColor(this->color);
+ ofNoFill();
+ ofDrawCircle(ofVec3f(), 0.35f / 2.0f);
+ }
+ ofPopStyle();
+ }
+ ofPopMatrix();
+
+ ofVboMesh mesh;
+ mesh.setMode(OF_PRIMITIVE_LINES);
+
+ //draw the rays for camera navigation
+ if (this->cameraNavigation.enabled) {
+ for (const auto & worldPoint : this->cameraNavigation.worldPoints) {
+ auto color = (ofFloatColor) this->color.get();
+ mesh.addVertex(this->cameraNavigation.cameraPosition);
+ mesh.addColor(color);
+
+ color.a = 0.0f;
+ mesh.addVertex(ofxCv::toOf(worldPoint));
+ mesh.addColor(color);
+ }
+ }
+
+ //draw the ray towards the test point where it lands on the mirror plane
+ {
+ mesh.addVertex(this->cameraNavigation.cameraPosition);
+ mesh.addColor(this->color.get());
+
+ mesh.addVertex(this->estimatedPointOnMirrorPlane);
+ mesh.addColor(this->color.get());
+ }
+
+ //draw the name if any
+ if (!this->name.empty()) {
+ ofDrawBitmapString(name, this->estimatedPointOnMirrorPlane);
+ }
+
+ ofPushStyle();
+ {
+ ofEnableAlphaBlending();
+ mesh.draw();
+ }
+ ofPopStyle();
+ }
+
+ //----------
+ std::string BoardInMirror::Capture::getDisplayString() const {
+ stringstream message;
+ if (!this->name.empty()) {
+ message << this->name << endl;
+ }
+
+ if (this->cameraNavigation.enabled) {
+ message << "Marker Map reprojection error : " << this->cameraNavigation.reprojectionError << endl;
+ }
+ message << "Real reprojection error : " << this->realPlane.reprojectionError << endl;
+ message << "Virtual reprojection error : " << this->virtualPlane.reprojectionError << endl;
+ return message.str();
+ }
+
+ //----------
+ void BoardInMirror::Capture::drawPlane(const Plane & plane) const {
+ ofPushMatrix();
+ {
+ ofMultMatrix(plane.boardTransform);
+ ofDrawAxis(0.1f);
+ }
+ ofPopMatrix();
+
+ ofPushStyle();
+ {
+ ofSetColor(this->color);
+ ofxCv::drawCorners(plane.worldPoints, false);
+ }
+ ofPopStyle();
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardInMirror.h b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardInMirror.h
new file mode 100644
index 0000000000..daae9db394
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardInMirror.h
@@ -0,0 +1,94 @@
+#pragma once
+
+#include "ofxRulr.h"
+#include "ofxNonLinearFit.h"
+#include "ofxRulr/Utils/CaptureSet.h"
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace MirrorPlaneCapture {
+ class BoardInMirror : public Base {
+ public:
+ class Capture : public Utils::AbstractCaptureSet::BaseCapture {
+ public:
+ void drawWorld();
+ string getDisplayString() const override;
+
+ struct {
+ bool enabled;
+ vector imagePoints;
+ vector worldPoints;
+ ofVec3f cameraPosition;
+ ofParameter reprojectionError{ "Reprojection error", 0 };
+ } cameraNavigation;
+
+ struct Plane {
+ vector imagePoints;
+ vector objectPoints;
+
+ ofxRay::Plane plane;
+ float reprojectionError;
+ float planeFitResidual;
+ ofMatrix4x4 boardTransform;
+ vector worldPoints;
+ ofVec3f worldPointCenter;
+
+ ofVec3f meanObjectPoint;
+ ofVec3f testPointWorld;
+ };
+
+ Plane realPlane;
+ Plane virtualPlane;
+
+ ofVec3f testObjectPoint;
+ ofVec3f estimatedPointOnMirrorPlane;
+ ofxRay::Plane mirrorPlane;
+
+ string name;
+ protected:
+ void drawPlane(const Plane &) const;
+ };
+
+ BoardInMirror();
+ string getTypeName() const override;
+ void init();
+ void update();
+ void addCapture();
+ void calibrate();
+ ofxCvGui::PanelPtr getPanel() override;
+ void populateInspector(ofxCvGui::InspectArguments &);
+ void drawWorldStage();
+
+ void addImage(const cv::Mat &, const string & name = "");
+ void addFolderOfImages(const std::filesystem::path & path);
+ protected:
+ struct : ofParameterGroup {
+ struct : ofParameterGroup {
+ ofParameter enabled{ "Enabled", true };
+ ofParameter minimumMarkers{ "Minimum markers", 3 };
+ PARAM_DECLARE("Camera navigation", enabled, minimumMarkers);
+ } cameraNavigation;
+
+ struct : ofParameterGroup {
+ ofParameter findBoardMode{ "Find board mode", FindBoardMode::Raw };
+ ofParameter minimumCorners{ "Minimum corners", 8 };
+ PARAM_DECLARE("Capture", findBoardMode, minimumCorners);
+ } capture;
+
+ struct : ofParameterGroup {
+ ofParameter enabled{ "Enabled", false };
+ ofParameter address{ "Address", "http://localhost:8000/Calibrate/LogServoValues"};
+ PARAM_DECLARE("Log to server", enabled, address);
+ } logToServer;
+
+ PARAM_DECLARE("SolveMirror", cameraNavigation, capture, logToServer);
+ } parameters;
+ shared_ptr panel;
+
+ Utils::CaptureSet captures;
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardOnMirror.cpp b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardOnMirror.cpp
new file mode 100644
index 0000000000..c451c901e5
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardOnMirror.cpp
@@ -0,0 +1,722 @@
+#include "pch_Plugin_Experiments.h"
+#include "planesToPoint.h"
+
+template
+T mean(const vector & points) {
+ T accumulator(0);
+
+ for (const auto & point : points) {
+ accumulator += point;
+ }
+ return accumulator / size(points);
+}
+
+#include "BoardOnMirror.h"
+
+float getMarkerReprojectionError(vector imagePoints, cv::Mat cameraMatrix, cv::Mat distortionCoefficients) {
+ cv::Mat translation, rotationVector;
+
+ vector dummyObjectPoints;
+ {
+ dummyObjectPoints.emplace_back(0, 0, 0);
+ dummyObjectPoints.emplace_back(1, 0, 0);
+ dummyObjectPoints.emplace_back(1, 1, 0);
+ dummyObjectPoints.emplace_back(0, 1, 0);
+ }
+
+ cv::solvePnP(dummyObjectPoints
+ , imagePoints
+ , cameraMatrix
+ , distortionCoefficients
+ , rotationVector
+ , translation);
+
+ vector reprojectedPoints;
+ cv::projectPoints(dummyObjectPoints
+ , rotationVector
+ , translation
+ , cameraMatrix
+ , distortionCoefficients
+ , reprojectedPoints);
+
+ //get RMS
+ float accumulate = 0.0f;
+ for (int i = 0; i < imagePoints.size(); i++) {
+ auto delta = reprojectedPoints[i] - imagePoints[i];
+ accumulate += delta.dot(delta);
+ }
+ auto MS = accumulate / imagePoints.size();
+ return sqrt(MS);
+}
+
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace MirrorPlaneCapture {
+ //----------
+ BoardOnMirror::BoardOnMirror() {
+ RULR_NODE_INIT_LISTENER;
+ }
+
+ //----------
+ string BoardOnMirror::getTypeName() const {
+ return "Experiments::MirrorPlaneCapture::BoardOnMirror";
+ }
+
+ //----------
+ void BoardOnMirror::init() {
+ RULR_NODE_DRAW_WORLD_LISTENER;
+ RULR_NODE_INSPECTOR_LISTENER;
+ RULR_NODE_UPDATE_LISTENER;
+ RULR_NODE_SERIALIZATION_LISTENERS;
+
+ this->addInput();
+ this->addInput();
+ this->addInput();
+ this->addInput();
+ this->addInput();
+
+ this->panel = make_shared();
+ this->captures.populateWidgets(this->panel);
+
+ this->manageParameters(this->parameters);
+ }
+
+ //----------
+ void BoardOnMirror::update() {
+
+ }
+
+ //----------
+ void BoardOnMirror::addCapture() {
+ this->throwIfMissingAConnection();
+
+ auto camera = this->getInput();
+ auto grabber = camera->getGrabber();
+ if (!grabber) {
+ throw(ofxRulr::Exception("Camera grabber not available"));
+ }
+
+ auto frame = grabber->getFreshFrame();
+ auto image = ofxCv::toCv(frame->getPixels());
+ this->addImage(image);
+ }
+
+ //----------
+ void BoardOnMirror::calibrate() {
+
+ }
+
+ //----------
+ ofxCvGui::PanelPtr BoardOnMirror::getPanel() {
+ return this->panel;
+ }
+
+ //----------
+ void BoardOnMirror::populateInspector(ofxCvGui::InspectArguments & inspectArgs) {
+ auto inspector = inspectArgs.inspector;
+ inspector->addButton("Add capture", [this]() {
+ try {
+ Utils::ScopedProcess scopedProcess("Add capture");
+ this->addCapture();
+ scopedProcess.end();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ }, ' ')->setHeight(100.0f);
+
+ inspector->addButton("Add folder of images...", [this]() {
+ auto result = ofSystemLoadDialog("Folder of calibration images", true);
+ if (result.bSuccess) {
+ this->addFolderOfImages(std::filesystem::path(result.getPath()));
+ }
+ });
+
+ inspector->addButton("Calibrate", [this]() {
+ try {
+ this->calibrate();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ }, OF_KEY_RETURN)->setHeight(100.0f);
+
+ inspector->addButton("Mean plane to clipboard", [this]() {
+ try {
+ this->copyMeanPlaneToClipboard();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ }, OF_KEY_RETURN)->setHeight(100.0f);
+
+ inspector->addButton("Project heliostats from planes", [this]() {
+ try {
+ this->projectToHeliostats();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ });
+ }
+
+ //----------
+ void BoardOnMirror::drawWorldStage() {
+ auto captures = this->captures.getSelection();
+ for (auto capture : captures) {
+ capture->drawWorld();
+ }
+ }
+
+ //----------
+ void BoardOnMirror::addImage(const cv::Mat & image, const string & name) {
+ this->throwIfMissingAConnection();
+ this->throwIfMissingAConnection();
+
+ auto camera = this->getInput();
+ auto board = this->getInput();
+
+ auto capture = make_shared();
+
+ capture->name = name;
+
+ //function to test reprojection error for a set of points given a translation, rotation
+ auto getReprojectionError = [&camera](const vector & objectPoints
+ , const vector & imagePoints
+ , const cv::Mat & rotationVector
+ , const cv::Mat & translation) {
+
+ float error = 0.0f;
+ vector reprojectedPoints;
+ cv::projectPoints(objectPoints
+ , rotationVector
+ , translation
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , reprojectedPoints);
+
+ //Take square mean
+ for (size_t i = 0; i < imagePoints.size(); i++) {
+ auto delta = imagePoints[i] - reprojectedPoints[i];
+ error += delta.x * delta.x + delta.y * delta.y;
+ }
+ //RMS
+ error /= (float)imagePoints.size();
+ error = sqrt(error);
+
+ return error;
+ };
+
+ //resolve pose of camera using the marker map
+ capture->cameraNavigation.enabled = this->parameters.cameraNavigation.enabled;
+ if (this->parameters.cameraNavigation.enabled) {
+ this->throwIfMissingAConnection();
+ this->throwIfMissingAConnection();
+
+ auto detector = this->getInput();
+ auto markerMap = this->getInput()->getMarkerMap();
+
+ auto markers = detector->findMarkers(image);
+ if (markers.size() < this->parameters.cameraNavigation.minimumMarkers) {
+ throw(ofxRulr::Exception("Found " + ofToString(markers.size()) + " real markers. Need " + ofToString(this->parameters.cameraNavigation.minimumMarkers) + " for camera navigation"));
+ }
+
+ //build up dataset
+ vector worldPoints;
+ vector imagePoints;
+ {
+ int countGoodMarkers = 0;
+ for (const auto & marker : markers) {
+ try {
+ //test reprojection error
+ auto markerReprojectionError = getMarkerReprojectionError((vector &) marker
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients());
+
+ if (markerReprojectionError <= this->parameters.cameraNavigation.maxMarkerError) {
+ countGoodMarkers++;
+ auto marker3D = markerMap->getMarker3DInfo(marker.id);
+ for (int i = 0; i < 4; i++) {
+ worldPoints.push_back(marker3D.points[i]);
+ imagePoints.push_back(marker[i]);
+ }
+ }
+ }
+ RULR_CATCH_ALL_TO_ERROR; //in the case that the marker did not exist
+ }
+
+ if (countGoodMarkers < this->parameters.cameraNavigation.minimumMarkers) {
+ throw(ofxRulr::Exception("Found " + ofToString(countGoodMarkers) + " good markers in map. Need " + ofToString(this->parameters.cameraNavigation.minimumMarkers) + " for camera navigation"));
+ }
+ }
+
+ cv::Mat rotationVector, translation;
+ camera->getExtrinsics(rotationVector, translation, true);
+
+ //Solve camera extrinsics
+ {
+ //first pass EPNP
+ cv::solvePnPRansac(worldPoints
+ , imagePoints
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , rotationVector
+ , translation
+ , true
+ , 100
+ , 5.0f
+ , 0.99
+ , cv::noArray()
+ , cv::SOLVEPNP_EPNP);
+
+ //second pass ITERATIVE
+ cv::solvePnPRansac(worldPoints
+ , imagePoints
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , rotationVector
+ , translation
+ , true
+ , 100
+ , 5.0f
+ , 0.99
+ , cv::noArray()
+ , cv::SOLVEPNP_ITERATIVE);
+ }
+
+ camera->setExtrinsics(rotationVector, translation, true);
+
+ capture->cameraNavigation.imagePoints = imagePoints;
+ capture->cameraNavigation.worldPoints = worldPoints;
+ capture->cameraNavigation.reprojectionError = getReprojectionError(worldPoints
+ , imagePoints
+ , rotationVector
+ , translation);
+ capture->cameraNavigation.cameraPosition = camera->getPosition();
+ }
+
+ auto cameraTransform = camera->getTransform();
+
+ //--
+ //find board plane function
+ //
+ //
+ auto findBoardPlane = [this, camera, board, &getReprojectionError, &cameraTransform](const cv::Mat & image, Capture::Plane & plane) {
+
+ //get board image and object space points
+ if (!board->findBoard(image
+ , plane.imagePoints
+ , plane.objectPoints
+ , this->parameters.capture.findBoardMode
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients())) {
+ throw(Exception("Failed to find board"));
+ }
+
+ //check we found enough corners
+ if (plane.objectPoints.size() < this->parameters.capture.minimumCorners) {
+ throw(Exception("Not enough corners found (" + ofToString(plane.objectPoints.size()) + " found. Needs " + ofToString(this->parameters.capture.minimumCorners) + ")"));
+ }
+
+ cv::Mat rotation, translation;
+ {
+ float acceptableReprojectionError = ofClamp(camera->getWidth() / 1000, 1, 10);
+
+ //second pass ITERATIVE
+ cv::solvePnP(plane.objectPoints
+ , plane.imagePoints
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , rotation
+ , translation
+ , false
+ , cv::SOLVEPNP_ITERATIVE);
+ }
+
+ //record reprojection error
+ plane.reprojectionError = getReprojectionError(plane.objectPoints
+ , plane.imagePoints
+ , rotation
+ , translation);
+
+ //build up world space points
+ plane.boardTransform = ofxCv::makeMatrix(rotation, translation) * cameraTransform;
+
+ for (const auto & objectPoint : plane.objectPoints) {
+ plane.worldPoints.push_back(ofxCv::toOf(objectPoint) * plane.boardTransform);
+ }
+
+ if (!plane.plane.fitToPoints(plane.worldPoints, plane.planeFitResidual)) {
+ throw(Exception("Failed to fit board plane"));
+ }
+
+ //take mean of object space points
+ plane.meanObjectPoint = mean(ofxCv::toOf(plane.objectPoints));
+ plane.meanWorldPoint = mean(plane.worldPoints);
+ plane.plane.setCenter(plane.meanWorldPoint);
+
+ //this seems to be the data we actually want. And the normal should be consistent
+ plane.plane.setNormal(-(ofVec3f) plane.boardTransform.getRowAsVec3f(2));
+ };
+ //
+ //--
+
+
+ //find board plane
+ findBoardPlane(image, capture->mirrorPlane);
+
+
+ //log to server
+ if (this->parameters.logToServer.enabled) {
+ try {
+ Utils::ScopedProcess scopedProcessLogToServer("Log to server");
+ Json::Value requestJson;
+
+ {
+ auto & data = requestJson["data"];
+
+ auto & jsonPlane = data["plane"];
+
+ jsonPlane["ABCD"] << capture->mirrorPlane.plane.getABCD();
+ jsonPlane["center"] << capture->mirrorPlane.plane.getCenter();
+ jsonPlane["normal"] << capture->mirrorPlane.plane.getNormal();
+ }
+
+ ofHttpRequest request(this->parameters.logToServer.address.get(), "log");
+ request.method = ofHttpRequest::Method::POST;
+ request.body = requestJson.toStyledString();
+ request.contentType = "application/json";
+
+ ofURLFileLoader urlLoader;
+ auto response = urlLoader.handleRequest(request);
+ cout << response.data;
+ scopedProcessLogToServer.end();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ }
+
+ this->captures.add(capture);
+ }
+
+ //----------
+ void BoardOnMirror::addFolderOfImages(const std::filesystem::path & path) {
+ //get list of files
+ vector filePaths;
+ for (std::filesystem::directory_iterator it(path)
+ ; it != std::filesystem::directory_iterator()
+ ; ++it) {
+ filePaths.push_back(it->path().string());
+ }
+
+ Utils::ScopedProcess scopedProcess("Adding folder images", true, filePaths.size());
+
+ //for each file in path
+ for (auto filePath : filePaths) {
+ try {
+ Utils::ScopedProcess fileScopedProcess(filePath, false);
+
+ //load image
+ auto image = cv::imread(filePath);
+
+ //check it loaded
+ if (image.empty()) {
+ continue;
+ }
+ else {
+ //add it to the captures
+ this->addImage(image, ofFilePath::getBaseName(filePath));
+ }
+ }
+ RULR_CATCH_ALL_TO({
+ RULR_WARNING << e.what();
+ });
+ }
+
+ scopedProcess.end();
+ }
+
+ //----------
+ void BoardOnMirror::copyMeanPlaneToClipboard() const {
+ auto captures = this->captures.getSelection();
+ if (captures.empty()) {
+ throw(Exception("No captures available"));
+ }
+
+ ofVec3f meanCenter, meanNormal;
+ for (auto capture : captures) {
+ meanCenter += capture->mirrorPlane.plane.getCenter();
+ meanNormal += capture->mirrorPlane.plane.getNormal();
+ }
+ meanCenter /= captures.size();
+ meanNormal /= captures.size();
+ meanNormal.normalize();
+
+ ofxRay::Plane meanPlane;
+ meanPlane.setCenter(meanCenter);
+ meanPlane.setNormal(meanNormal);
+
+ stringstream ss;
+ ss << meanPlane.getABCD();
+ ofxClipboard::copy(ss.str());
+ }
+
+ //----------
+ void BoardOnMirror::projectToHeliostats() {
+ this->throwIfMissingAConnection();
+ auto heliostatsNode = this->getInput();
+
+ //get our selected captures
+ auto captures = this->captures.getSelection();
+
+ //existing heliostats
+ auto existingHeliostats = heliostatsNode->getHeliostats();
+
+ set> heliostatsToAdd;
+
+ map, vector>> capturesToMove;
+
+ for (auto capture : captures) {
+ auto backPosition = capture->mirrorPlane.meanWorldPoint
+ - capture->mirrorPlane.plane.getNormal()
+ * this->parameters.heliostatProjection.planeToA2;
+
+ //check if matching heliostat already exists
+ shared_ptr heliostat;
+ for (auto heliostatOther : existingHeliostats) {
+ if ((heliostatOther->position.get() - backPosition).length() < this->parameters.heliostatProjection.distanceThreshold) {
+ heliostat = heliostatOther;
+ break;
+ }
+ }
+
+ //if it doesn't already exist, check if it's one we are already trying to add
+ if (!heliostat) {
+ for (auto heliostatOther : heliostatsToAdd) {
+ if ((heliostatOther->position.get() - backPosition).length() < this->parameters.heliostatProjection.distanceThreshold) {
+ heliostat = heliostatOther;
+ break;
+ }
+ }
+ }
+
+ //if it doesn't already exist, create it
+ if (!heliostat) {
+ heliostat = make_shared();
+ heliostat->position = backPosition;
+
+
+ static int index = 0;
+ heliostat->name = "H-auto" + ofToString(index++);
+
+ heliostatsToAdd.insert(heliostat);
+ }
+
+ auto dictIt = capturesToMove.find(heliostat);
+ if (dictIt == capturesToMove.end()) {
+ capturesToMove.emplace(heliostat, vector>());
+ dictIt = capturesToMove.find(heliostat);
+ }
+
+ dictIt->second.push_back(capture);
+ }
+
+ //kill heliostats which aren't full enough
+ for (auto it = heliostatsToAdd.begin(); it != heliostatsToAdd.end();) {
+ auto heliostat = *it;
+ if (capturesToMove[heliostat].size() < this->parameters.heliostatProjection.minimumCaptures) {
+ it = heliostatsToAdd.erase(it);
+ }
+ else {
+ it++;
+ }
+ }
+
+ //move captures
+ for (auto it : capturesToMove) {
+ auto heliostat = it.first;
+ auto captures = it.second;
+
+ //move the captures we moved from our set to that set
+ for (auto capture : captures) {
+ //clone by serialization
+ Json::Value json;
+ capture->serialize(json);
+ auto captureClone = make_shared();
+ captureClone->deserialize(json);
+ heliostat->captures.add(captureClone);
+
+ capture->onDeletePressed.notifyListeners();
+ }
+
+ //add them to the heliostats node if it's in the set
+ //if (heliostatsToAdd.find(heliostat) != heliostatsToAdd.end()) {
+ //HACK WTF?
+ heliostatsNode->add(heliostat);
+ //}
+
+ //recalc centers for heliostats
+ heliostat->calcPosition(this->parameters.heliostatProjection.planeToA2);
+ }
+ }
+
+ //----------
+ void BoardOnMirror::serialize(Json::Value & json) {
+ this->captures.serialize(json["captures"]);
+ }
+
+ //----------
+ void BoardOnMirror::deserialize(const Json::Value & json) {
+ this->captures.deserialize(json["captures"]);
+ }
+
+#pragma mark Capture
+ //----------
+ BoardOnMirror::Capture::Capture() {
+ RULR_SERIALIZE_LISTENERS;
+ }
+
+ //----------
+ void BoardOnMirror::Capture::drawWorld() {
+ this->drawPlane(this->mirrorPlane);
+
+ //draw the mirror
+ ofPushMatrix();
+ {
+ ofQuaternion rotation;
+ rotation.makeRotate(ofVec3f(0, 0, 1), this->mirrorPlane.plane.getNormal());
+
+ ofTranslate(this->mirrorPlane.plane.getCenter());
+ ofMultMatrix(ofMatrix4x4(rotation));
+
+ ofPushStyle();
+ {
+ ofSetColor(this->color);
+ ofNoFill();
+ ofDrawCircle(ofVec3f(), 0.35f / 2.0f);
+ }
+ ofPopStyle();
+ }
+ ofPopMatrix();
+
+ ofVboMesh mesh;
+ mesh.setMode(OF_PRIMITIVE_LINES);
+
+ //draw the rays for camera navigation
+ if (this->cameraNavigation.enabled) {
+ for (const auto & worldPoint : this->cameraNavigation.worldPoints) {
+ auto color = (ofFloatColor) this->color.get();
+ mesh.addVertex(this->cameraNavigation.cameraPosition);
+ mesh.addColor(color);
+
+ color.a = 0.0f;
+ mesh.addVertex(ofxCv::toOf(worldPoint));
+ mesh.addColor(color);
+ }
+ }
+
+ //draw the ray towards the test point where it lands on the mirror plane
+ {
+ mesh.addVertex(this->cameraNavigation.cameraPosition);
+ mesh.addColor(this->color.get());
+
+ mesh.addVertex(this->mirrorPlane.meanWorldPoint);
+ mesh.addColor(this->color.get());
+ }
+
+ //draw the name if any
+ if (!this->name.empty()) {
+ ofDrawBitmapString(name, this->mirrorPlane.meanWorldPoint);
+ }
+
+ ofPushStyle();
+ {
+ ofEnableAlphaBlending();
+ mesh.draw();
+ }
+ ofPopStyle();
+ }
+
+ //----------
+ std::string BoardOnMirror::Capture::getDisplayString() const {
+ stringstream message;
+ if (!this->name.empty()) {
+ message << this->name << endl;
+ }
+
+ if (this->cameraNavigation.enabled) {
+ message << "Marker Map reprojection error : " << this->cameraNavigation.reprojectionError << endl;
+ }
+ message << "'Board reprojection error : " << this->mirrorPlane.reprojectionError << endl;
+ return message.str();
+ }
+
+ //----------
+ void BoardOnMirror::Capture::serialize(Json::Value & json) {
+ json["name"] << this->name;
+ json["heliostatName"] << this->heliostatName;
+
+ {
+ auto & jsonCameraNavigation = json["cameraNavigation"];
+ jsonCameraNavigation["enabled"] = this->cameraNavigation.enabled;
+ jsonCameraNavigation["imagePoints"] << ofxCv::toOf(this->cameraNavigation.imagePoints);
+ jsonCameraNavigation["worldPoints"] << ofxCv::toOf(this->cameraNavigation.worldPoints);
+ jsonCameraNavigation["cameraPosition"] << this->cameraNavigation.cameraPosition;
+ Utils::Serializable::serialize(jsonCameraNavigation, this->cameraNavigation.reprojectionError);
+ }
+
+ {
+ auto & jsonPlane = json["mirrorPlane"];
+ jsonPlane["imagePoints"] << ofxCv::toOf(this->mirrorPlane.imagePoints);
+ jsonPlane["worldPoints"] << ofxCv::toOf(this->mirrorPlane.objectPoints);
+ jsonPlane["plane"] << this->mirrorPlane.plane;
+ jsonPlane["reprojectionError"] << this->mirrorPlane.reprojectionError;
+ jsonPlane["planeFitResidual"] << this->mirrorPlane.planeFitResidual;
+ jsonPlane["boardTransform"] << this->mirrorPlane.boardTransform;
+ jsonPlane["worldPoints"] << this->mirrorPlane.worldPoints;
+ jsonPlane["meanObjectPoint"] << this->mirrorPlane.meanObjectPoint;
+ jsonPlane["meanWorldPoint"] << this->mirrorPlane.meanWorldPoint;
+ }
+ }
+
+ //----------
+ void BoardOnMirror::Capture::deserialize(const Json::Value & json) {
+ json["name"] >> this->name;
+ json["heliostatName"] >> this->heliostatName;
+
+ {
+ const auto & jsonCameraNavigation = json["cameraNavigation"];
+ this->cameraNavigation.enabled = jsonCameraNavigation["enabled"].asBool();
+ jsonCameraNavigation["imagePoints"] >> ofxCv::toOf(this->cameraNavigation.imagePoints);
+ jsonCameraNavigation["worldPoints"] >> ofxCv::toOf(this->cameraNavigation.worldPoints);
+ jsonCameraNavigation["cameraPosition"] >> this->cameraNavigation.cameraPosition;
+ Utils::Serializable::deserialize(jsonCameraNavigation, this->cameraNavigation.reprojectionError);
+ }
+
+ {
+ const auto & jsonPlane = json["mirrorPlane"];
+ jsonPlane["imagePoints"] >> ofxCv::toOf(this->mirrorPlane.imagePoints);
+ jsonPlane["worldPoints"] >> ofxCv::toOf(this->mirrorPlane.objectPoints);
+ jsonPlane["plane"] >> this->mirrorPlane.plane;
+ jsonPlane["reprojectionError"] >> this->mirrorPlane.reprojectionError;
+ jsonPlane["planeFitResidual"] >> this->mirrorPlane.planeFitResidual;
+ jsonPlane["boardTransform"] >> this->mirrorPlane.boardTransform;
+ jsonPlane["worldPoints"] >> this->mirrorPlane.worldPoints;
+ jsonPlane["meanObjectPoint"] >> this->mirrorPlane.meanObjectPoint;
+ jsonPlane["meanWorldPoint"] >> this->mirrorPlane.meanWorldPoint;
+ }
+ }
+
+ //----------
+ void BoardOnMirror::Capture::drawPlane(const Plane & plane) const {
+ ofPushMatrix();
+ {
+ ofMultMatrix(plane.boardTransform);
+ ofDrawAxis(0.1f);
+ }
+ ofPopMatrix();
+
+ ofPushStyle();
+ {
+ ofSetColor(this->color);
+ ofxCv::drawCorners(plane.worldPoints, false);
+ }
+ ofPopStyle();
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardOnMirror.h b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardOnMirror.h
new file mode 100644
index 0000000000..80c563d014
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/BoardOnMirror.h
@@ -0,0 +1,113 @@
+#pragma once
+
+#include "ofxRulr.h"
+#include "ofxNonLinearFit.h"
+#include "ofxRulr/Utils/CaptureSet.h"
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace MirrorPlaneCapture {
+ class BoardOnMirror : public Base {
+ public:
+ class Capture : public Utils::AbstractCaptureSet::BaseCapture {
+ public:
+ Capture();
+ void drawWorld();
+ string getDisplayString() const override;
+
+ struct {
+ bool enabled;
+ vector imagePoints;
+ vector worldPoints;
+ ofVec3f cameraPosition;
+ ofParameter reprojectionError{ "Reprojection error", 0 };
+ } cameraNavigation;
+
+ struct Plane {
+ vector imagePoints;
+ vector objectPoints;
+
+ ofxRay::Plane plane;
+ float reprojectionError;
+ float planeFitResidual;
+ ofMatrix4x4 boardTransform;
+ vector worldPoints;
+
+ ofVec3f meanObjectPoint;
+ ofVec3f meanWorldPoint;
+ };
+
+ Plane mirrorPlane;
+
+ string name;
+ string heliostatName;
+
+ void serialize(Json::Value &);
+ void deserialize(const Json::Value &);
+ protected:
+ void drawPlane(const Plane &) const;
+ };
+
+ BoardOnMirror();
+ string getTypeName() const override;
+ void init();
+ void update();
+ void addCapture();
+ void calibrate();
+ ofxCvGui::PanelPtr getPanel() override;
+ void populateInspector(ofxCvGui::InspectArguments &);
+ void drawWorldStage();
+
+ void addImage(const cv::Mat &, const string & name = "");
+ void addFolderOfImages(const std::filesystem::path & path);
+
+ void copyMeanPlaneToClipboard() const;
+
+ void projectToHeliostats();
+
+ void serialize(Json::Value &);
+ void deserialize(const Json::Value &);
+ protected:
+ struct : ofParameterGroup {
+ struct : ofParameterGroup {
+ ofParameter enabled{ "Enabled", true };
+ ofParameter minimumMarkers{ "Minimum markers", 3 };
+ ofParameter maxMarkerError{ "Max marker error", 0.5f };
+ PARAM_DECLARE("Camera navigation", enabled, minimumMarkers, maxMarkerError);
+ } cameraNavigation;
+
+ struct : ofParameterGroup {
+ ofParameter findBoardMode{ "Find board mode", FindBoardMode::Raw };
+ ofParameter minimumCorners{ "Minimum corners", 8 };
+ PARAM_DECLARE("Capture", findBoardMode, minimumCorners);
+ } capture;
+
+ struct : ofParameterGroup {
+ ofParameter planeToA2{ "Plane to A2 [m]", 0.15f };
+ ofParameter distanceThreshold{ "Distance threshold [m]", 0.2f };
+ ofParameter minimumCaptures{ "Minimum captures", 2 };
+ PARAM_DECLARE("Heliostat projection", distanceThreshold, minimumCaptures);
+ } heliostatProjection;
+
+// struct : ofParameterGroup {
+// ofParameter navigation{ "Navigation", WhenDrawOnWorldStage::Selected };
+// PARAM_DECLARE("Draw", navigation);
+// } draw;
+
+ struct : ofParameterGroup {
+ ofParameter enabled{ "Enabled", false };
+ ofParameter address{ "Address", "http://localhost:8000/Calibrate/LogServoValues" };
+ PARAM_DECLARE("Log to server", enabled, address);
+ } logToServer;
+
+ PARAM_DECLARE("SolveMirror", cameraNavigation, capture, heliostatProjection, logToServer);
+ } parameters;
+ shared_ptr panel;
+
+ Utils::CaptureSet captures;
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/HaloBoard.cpp b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/HaloBoard.cpp
new file mode 100644
index 0000000000..b7274b6b9e
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/HaloBoard.cpp
@@ -0,0 +1,555 @@
+#include "pch_Plugin_Experiments.h"
+
+#define INCHES_PER_METER (1.0f / 0.0254f)
+
+#define ARUCO_MIP_16h3_NonMirroring {0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 78, 79, 80, 81 }
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace MirrorPlaneCapture {
+ //----------
+ HaloBoard::HaloBoard() {
+ RULR_NODE_INIT_LISTENER;
+ }
+
+ //----------
+ std::string HaloBoard::getTypeName() const {
+ return "ArUco::HaloBoard";
+ }
+
+ //----------
+ void HaloBoard::init() {
+ RULR_NODE_UPDATE_LISTENER;
+
+ auto panel = ofxCvGui::Panels::makeImage(this->preview);
+ panel->onDrawImage += [this](ofxCvGui::DrawImageArguments & args) {
+ //paper sizes
+ ofPushMatrix();
+ {
+ float mmToPixels = 1 / 1000.0f * INCHES_PER_METER * this->parameters.preview.DPI;
+ ofScale(mmToPixels, mmToPixels);
+
+ ofColor baseColor(200, 100, 100);
+ int index = 0;
+ for (auto paperSize : this->paperSizes) {
+ auto paperColor = baseColor;
+ paperColor.setHue(index * 40);
+
+ ofPushStyle();
+ {
+ ofSetColor(paperColor);
+
+ //outline
+ ofNoFill();
+ ofDrawRectangle(0, 0, 297, 210);
+
+ //text
+ ofDrawBitmapString(paperSize->name, paperSize->width, paperSize->height);
+ }
+ ofPopStyle();
+
+ index++;
+ }
+ }
+ ofPopMatrix();
+ };
+ this->panel = panel;
+
+ this->manageParameters(this->parameters);
+
+ {
+ this->paperSizes.emplace(new PaperSize{ "A0", 1189, 841 });
+ this->paperSizes.emplace(new PaperSize{ "A1", 841, 594 });
+ this->paperSizes.emplace(new PaperSize{ "A2", 594, 420 });
+ this->paperSizes.emplace(new PaperSize{ "A3", 420, 297 });
+ this->paperSizes.emplace(new PaperSize{ "A4", 297, 210 });
+ }
+
+ this->dictionary = aruco::Dictionary::load("ARUCO_MIP_16h3");
+
+ this->detector = make_shared(this->dictionary.getName());
+ {
+ auto & params = this->detector->getParameters();
+ params.enclosedMarker = true;
+ }
+ }
+
+ //----------
+ void HaloBoard::update() {
+ //delete preview if it's wrong
+ if (this->parameters.size.width != this->cachedParameters.size.width
+ || this->parameters.size.height != this->cachedParameters.size.height
+
+ || this->parameters.length.square != this->cachedParameters.length.square
+ || this->parameters.length.outerCorner != this->cachedParameters.length.outerCorner
+
+ || this->parameters.preview.DPI != this->cachedParameters.preview.DPI) {
+ //we need to rebuild the preview
+ this->board.reset();
+ }
+
+ //rebuild the board if needed
+ if (!this->board) {
+ try {
+ auto board = make_shared();
+
+ // Function for adding corners
+ auto addCorner = [board](const Board::Position & position, const Board::MarkerCorner & markerCorner) {
+ //find if corner already exists
+ Board::BoardCorner * cornerData = nullptr;
+
+ for (auto & corner : board->corners) {
+ if (corner.position == position) {
+ cornerData = &corner;
+ }
+ }
+
+ //make one if not
+ if (!cornerData) {
+ board->corners.push_back(Board::BoardCorner());
+ cornerData = &board->corners.back();
+ cornerData->position = position;
+ }
+
+ cornerData->markerCorners.push_back(markerCorner);
+ };
+
+ // Allocate the fbo
+ ofFbo previewFbo;
+ {
+ auto pixelSize = this->getPhysicalSize() * this->getPreviewPixelsPerMeter();
+ ofFbo::Settings fboSettings;
+ {
+ fboSettings.width = pixelSize.x;
+ fboSettings.height = pixelSize.y;
+ fboSettings.internalformat = GL_RGBA;
+ };
+
+ previewFbo.allocate(fboSettings);
+ }
+
+ previewFbo.begin();
+ {
+ ofClear(255, 255);
+
+ ofPushMatrix();
+ {
+ //change into meters
+ auto pixelsPerMeter = this->getPreviewPixelsPerMeter();
+ ofScale(pixelsPerMeter, pixelsPerMeter);
+
+ //move in by the outer corner amount
+ ofTranslate(this->parameters.length.outerCorner, this->parameters.length.outerCorner);
+
+ //scale by square
+ const auto square = this->parameters.length.square;
+ ofScale(square, square);
+
+ auto markerIDs = this->getNonMirroringIDs();
+ auto markerIDIterator = markerIDs.begin();
+
+ ofImage markerPreview;
+
+ //Iterate over squares
+ for (int y = 0; y < this->parameters.size.height; y++) {
+ for (int x = 0; x < this->parameters.size.width; x++) {
+ if (x % 2 == y % 2) {
+ //active squares
+
+ if (markerIDIterator == markerIDs.end()) {
+ throw(Exception("Not enough markers available in dictionary for this board design"));
+ }
+
+ auto markerID = *markerIDIterator++;
+
+ //make the marker image
+ {
+ auto markerImage = this->dictionary.getMarkerImage_id(markerID
+ , 1
+ , false
+ , false
+ , false);
+ cv::Mat markerImageColor;
+ cv::cvtColor(markerImage, markerImageColor, CV_GRAY2RGBA);
+ ofxCv::copy(markerImageColor, markerPreview);
+ markerPreview.update();
+
+ // Turn off filtering
+ markerPreview.getTexture().setTextureMinMagFilter(GL_NEAREST, GL_NEAREST);
+ }
+
+ //render the image into the fbo
+ markerPreview.draw(x, y, 1, 1);
+
+ //Store the data
+ {
+ //Marker
+ {
+ Board::Marker marker{
+ markerID,
+ Board::Position {
+ x,
+ y
+ }
+ };
+
+ board->markers.emplace(markerID, marker);
+ }
+
+ //Corners
+ {
+ addCorner(Board::Position{ x, y }, Board::MarkerCorner{ markerID, 0 });
+ addCorner(Board::Position{ x + 1, y }, Board::MarkerCorner{ markerID, 1 });
+ addCorner(Board::Position{ x + 1, y + 1 }, Board::MarkerCorner{ markerID, 2 });
+ addCorner(Board::Position{ x, y + 1 }, Board::MarkerCorner{ markerID, 3 });
+ }
+ }
+ }
+ }
+ }
+
+ //Draw the outside parts
+ {
+ auto outsideSize = this->parameters.length.outerCorner / this->parameters.length.square;
+
+ ofPushStyle();
+ {
+ ofFill();
+ ofSetLineWidth(0);
+ ofSetColor(0, 255);
+
+ auto width = this->parameters.size.width.get();
+ auto height = this->parameters.size.height.get();
+
+ //top left
+ {
+ ofPath arc;
+ arc.setCircleResolution(100);
+ arc.arc(ofVec2f(0, 0), outsideSize, outsideSize, 180, 270);
+ arc.setFillColor(ofColor(0));
+ arc.setFilled(true);
+ arc.close();
+ arc.draw();
+ }
+
+ //bottom left
+ if (height % 2 == 1) {
+ ofPath arc;
+ arc.setCircleResolution(100);
+ arc.arc(ofVec2f(0, height), outsideSize, outsideSize, 90, 180);
+ arc.setFillColor(ofColor(0));
+ arc.setFilled(true);
+ arc.close();
+ arc.draw();
+ }
+
+ //top right
+ if (width % 2 == 1) {
+ ofPath arc;
+ arc.setCircleResolution(100);
+ arc.arc(ofVec2f(width, 0), outsideSize, outsideSize, 270, 360);
+ arc.setFillColor(ofColor(0));
+ arc.setFilled(true);
+ arc.close();
+ arc.draw();
+ }
+
+ //bottom right
+ if (width % 2 == 1 && height % 2 == 1) {
+ ofPath arc;
+ arc.setCircleResolution(100);
+ arc.arc(ofVec2f(width, height), outsideSize, outsideSize, 0, 90);
+ arc.setFillColor(ofColor(0));
+ arc.setFilled(true);
+ arc.close();
+ arc.draw();
+ }
+
+ //top edge
+ for (int x = 0; x < width; x++) {
+ if (x % 2 == 1) {
+ ofDrawRectangle(x, -outsideSize, 1, outsideSize);
+ }
+ }
+
+ //left edge
+ for (int y = 0; y < height; y++) {
+ if (y % 2 == 1) {
+ ofDrawRectangle(-outsideSize, y, outsideSize, 1);
+ }
+ }
+
+ //bottom edge
+ for (int x = 0; x < width; x++) {
+ if (x % 2 == height % 2) {
+ ofDrawRectangle(x, height, 1, outsideSize);
+ }
+ }
+
+ //right edge
+ for (int y = 0; y < height; y++) {
+ if (y % 2 == width % 2) {
+ ofDrawRectangle(width, y, outsideSize, 1);
+ }
+ }
+ }
+ ofPopStyle();
+ }
+ }
+ ofPopMatrix();
+ }
+ previewFbo.end();
+
+ //copy the preview into the image
+ {
+ previewFbo.readToPixels(this->preview.getPixels());
+ this->preview.update();
+ }
+
+ //sort the board
+ {
+ sort(board->corners.begin(), board->corners.end(),
+ [](const Board::BoardCorner & a, const Board::BoardCorner & b) -> bool
+ {
+ if (a.position.y == b.position.y) {
+ return a.position.x < b.position.x;
+ }
+ else {
+ return a.position.y < b.position.y;
+ }
+
+ });
+ }
+
+ //save the board
+ this->board = board;
+
+ //cache parameters
+ {
+ this->cachedParameters.size.width = this->parameters.size.width;
+ this->cachedParameters.size.height = this->parameters.size.height;
+
+ this->cachedParameters.length.square = this->parameters.length.square;
+ this->cachedParameters.length.outerCorner = this->parameters.length.outerCorner;
+
+ this->cachedParameters.preview.DPI = this->parameters.preview.DPI;
+ }
+ }
+ RULR_CATCH_ALL_TO_ERROR
+ }
+ }
+
+ //----------
+ void HaloBoard::serialize(Json::Value & json) {
+ Utils::Serializable::serialize(json, this->parameters);
+ }
+
+ //----------
+ void HaloBoard::deserialize(const Json::Value & json) {
+ Utils::Serializable::deserialize(json, this->parameters);
+ }
+
+ //----------
+ void HaloBoard::populateInspector(ofxCvGui::InspectArguments & inspectArgs) {
+ auto inspector = inspectArgs.inspector;
+ inspector->addParameterGroup(this->parameters);
+ inspector->addLiveValue("Physical size [m]", [this]() {
+ return this->getPhysicalSize();
+ });
+ }
+
+ //----------
+ //https://github.com/opencv/opencv_contrib/blob/master/modules/aruco/samples/detect_board_charuco.cpp
+ bool HaloBoard::findBoard(cv::Mat image, vector & imagePoints, vector & objectPoints, FindBoardMode, cv::Mat cameraMatrix, cv::Mat distortionCoefficients) const {
+ if (!this->board) {
+ throw(Exception("Board is not setup"));
+ }
+
+ cv::Mat imageGrey;
+ if (image.channels() == 1) {
+ imageGrey = image;
+ }
+ else {
+ cv::cvtColor(image, imageGrey, CV_RGB2GRAY);
+ }
+
+ if (this->parameters.detection.openCorners > 0) {
+ //get the corners in the image
+ vector corners;
+ cv::goodFeaturesToTrack(imageGrey
+ , corners
+ , 2000
+ , 0.01
+ , 10);
+
+ //draw circles over the corners
+ for (auto & corner : corners) {
+ cv::circle(imageGrey
+ , corner
+ , this->parameters.detection.openCorners
+ , cv::Scalar(255)
+ , -1);
+ }
+ }
+
+ imagePoints.clear();
+ objectPoints.clear();
+
+ //Find the markers
+ vector foundMarkers;
+ {
+ this->detector->detect(imageGrey
+ , foundMarkers
+ , cameraMatrix
+ , distortionCoefficients
+ , this->parameters.length.square);
+
+ if (foundMarkers.empty()) {
+ return false;
+ }
+ }
+
+ //Corner finds
+ map> markerCornerFinds;
+ vector markerCornerScale; // scale in pixels for a square meeting at this corner (for subpix)
+
+ for (const auto & marker : foundMarkers) {
+
+ //for each corner of a found marker
+ for (int i = 0; i < 4; i++) {
+ Board::MarkerCorner markerCorner{
+ marker.id,
+ i
+ };
+
+ //for each corner on the board
+ for (int boardCornerIndex = 0; boardCornerIndex < this->board->corners.size(); boardCornerIndex++) {
+ const auto & boardCorner = this->board->corners[boardCornerIndex];
+
+ //check if that corner matches this MarkerCorner
+ for (const auto & boardMarkerCorner : boardCorner.markerCorners) {
+ if (boardMarkerCorner == markerCorner) {
+ //store it into there
+ markerCornerFinds[boardCornerIndex].push_back(marker[i]);
+
+ //get the length of a square side meeting this corner
+ markerCornerScale.push_back((ofxCv::toOf(marker[(i + 1) % 4]) - ofxCv::toOf(marker[i])).length());
+
+ //save some time, nothing else should match in this loop
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ {
+ const auto square = this->parameters.length.square.get();
+ for (const auto & it : markerCornerFinds) {
+ const auto & boardCorner = this->board->corners[it.first];
+
+ //take the mean
+ ofVec2f mean;
+ {
+ ofVec2f accumulate;
+ for (const auto & find : it.second) {
+ accumulate += ofxCv::toOf(find);
+ }
+ mean = accumulate / it.second.size();
+ }
+
+ const auto & position = this->board->corners[it.first].position;
+
+ imagePoints.push_back(ofxCv::toCv(mean));
+ objectPoints.push_back(ofxCv::toCv(ofVec3f(position.x, position.y, 0.0f) * square));
+ }
+ }
+
+ if (imagePoints.empty()) {
+ return false;
+ }
+
+ //perform sub-pixel refinement
+ if(this->parameters.refinement.enabled) {
+ float mean = 0.0f;
+ {
+ float accumulate = 0.0f;
+ for (const auto & find : markerCornerScale) {
+ accumulate += find;
+ }
+ mean = accumulate / markerCornerScale.size();
+ }
+
+ int subPixSearch = mean / 8.0f; //markers are 6x6 pixels (4x4 data)
+
+ subPixSearch = (subPixSearch / 2) * 2 + 1; // ensure odd
+ if (subPixSearch < 1) {
+ subPixSearch = 1; // ensure non zero
+ }
+
+ int zeroZone = mean * 0.01f;
+
+ cv::cornerSubPix(imageGrey
+ , imagePoints
+ , cv::Size(subPixSearch
+ , subPixSearch)
+ , cv::Size(zeroZone
+ , zeroZone)
+ , cv::TermCriteria(CV_TERMCRIT_ITER + CV_TERMCRIT_EPS, 50, 1e-6));
+ }
+
+ return true;
+ }
+
+ //----------
+ void HaloBoard::drawObject() const {
+ if (this->board) {
+ auto pixelsPerMeter = this->getPreviewPixelsPerMeter();
+ auto square = this->parameters.length.square;
+ auto outerCorner = this->parameters.length.outerCorner;
+
+ this->preview.draw(-outerCorner
+ , -outerCorner
+ , square * this->parameters.size.width + 2 * outerCorner
+ , square * this->parameters.size.height + 2 * outerCorner);
+ }
+ }
+
+ //----------
+ float HaloBoard::getSpacing() const {
+ return this->parameters.length.square.get();
+ }
+
+ //----------
+ ofVec3f HaloBoard::getCenter() const {
+ return ofVec3f(this->parameters.size.width * this->parameters.length.square
+ , this->parameters.size.height * this->parameters.length.square
+ , 0.0f) / 2.0f;
+ }
+
+ //----------
+ ofxCvGui::PanelPtr HaloBoard::getPanel() {
+ return this->panel;
+ }
+
+ //----------
+ ofVec2f HaloBoard::getPhysicalSize() const {
+ return ofVec2f(this->parameters.size.width * this->parameters.length.square
+ , this->parameters.size.height * this->parameters.length.square)
+ + (this->parameters.length.outerCorner * 2);
+ }
+
+ //----------
+ std::vector HaloBoard::getNonMirroringIDs() const {
+ return vector ARUCO_MIP_16h3_NonMirroring;
+ }
+
+ //----------
+ float HaloBoard::getPreviewPixelsPerMeter() const {
+ return INCHES_PER_METER * this->parameters.preview.DPI;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/HaloBoard.h b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/HaloBoard.h
new file mode 100644
index 0000000000..b2d9f1be41
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/HaloBoard.h
@@ -0,0 +1,125 @@
+#pragma once
+
+#include "Constants_Plugin_ArUco.h"
+#include "ofxRulr/Nodes/Item/AbstractBoard.h"
+#include
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace MirrorPlaneCapture {
+ class HaloBoard : public Nodes::Item::AbstractBoard {
+ public:
+ struct PaperSize {
+ string name;
+ float width;
+ float height;
+ };
+
+ HaloBoard();
+ string getTypeName() const override;
+ void init();
+ void update();
+ void serialize(Json::Value &);
+ void deserialize(const Json::Value &);
+ void populateInspector(ofxCvGui::InspectArguments &);
+
+ bool findBoard(cv::Mat, vector & imagePoints, vector & objectPoints, FindBoardMode findBoardMode, cv::Mat cameraMatrix, cv::Mat distortionCoefficients) const override;
+ void drawObject() const override;
+ float getSpacing() const override;
+ ofVec3f getCenter() const;
+
+ ofxCvGui::PanelPtr getPanel() override;
+
+ ofVec2f getPhysicalSize() const;
+
+ vector getNonMirroringIDs() const;
+
+ protected:
+ float getPreviewPixelsPerMeter() const;
+
+ struct Parameters : ofParameterGroup {
+ struct : ofParameterGroup {
+ ofParameter width{ "Width", 5 };
+ ofParameter height{ "Height", 5 };
+ PARAM_DECLARE("Size [squares]", width, height);
+ } size;
+
+ struct : ofParameterGroup {
+ ofParameter square{ "Square", 0.036, 0.001, 0.10 };
+ ofParameter outerCorner{ "Outer corner", 0.01, 0, 0.1 };
+ PARAM_DECLARE("Length [m]", square, outerCorner);
+ } length;
+
+ struct : ofParameterGroup {
+ ofParameter DPI{ "Preview DPI", 75 };
+ ofParameter showPaperSizes{ "Show paper sizes", false };
+ PARAM_DECLARE("Preview", DPI, showPaperSizes);
+ } preview;
+
+ struct : ofParameterGroup {
+ ofParameter refineStrategy{ "Refine strategy", true };
+ ofParameter errorCorrectionRate{ "Error correction rate", 0.6 };
+ ofParameter openCorners{ "Open corners [px]", 0 };
+ PARAM_DECLARE("Detection", refineStrategy, errorCorrectionRate, openCorners);
+ } detection;
+
+ struct : ofParameterGroup {
+ ofParameter enabled{ "Enabled", true };
+ PARAM_DECLARE("Refinement", enabled);
+ } refinement;
+
+ PARAM_DECLARE("HaloBoard", size, length, preview, detection, refinement);
+ };
+
+ struct Board {
+ struct Position {
+ int x;
+ int y;
+
+ bool operator==(const Position & other) const {
+ return other.x == this->x && other.y == this->y;
+ }
+ };
+
+ struct Marker {
+ int id;
+ Position position;
+ };
+
+ struct MarkerCorner {
+ int markerID;
+ int cornerIndex;
+
+ bool operator==(const MarkerCorner & other) const {
+ return other.markerID == this->markerID && other.cornerIndex == this->cornerIndex;
+ }
+ };
+
+ struct BoardCorner {
+ Position position;
+ vector markerCorners;
+ };
+
+ map markers;
+ vector corners;
+ };
+
+ Parameters parameters;
+ Parameters cachedParameters;
+
+ aruco::Dictionary dictionary;
+ shared_ptr detector;
+ float cachedDPI = 0;
+
+ shared_ptr board;
+ ofImage preview;
+
+ ofxCvGui::PanelPtr panel;
+
+ set> paperSizes;
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/Heliostats.cpp b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/Heliostats.cpp
new file mode 100644
index 0000000000..92ff8b64fb
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/Heliostats.cpp
@@ -0,0 +1,227 @@
+#include "pch_Plugin_Experiments.h"
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace MirrorPlaneCapture {
+#pragma mark Heliostats
+ //----------
+ Heliostats::Heliostats() {
+ RULR_NODE_INIT_LISTENER;
+ }
+
+ //----------
+ string Heliostats::getTypeName() const {
+ return "Halo::Heliostats";
+ }
+
+ //----------
+ void Heliostats::init() {
+ RULR_NODE_INSPECTOR_LISTENER;
+ RULR_NODE_DRAW_WORLD_LISTENER;
+ RULR_NODE_SERIALIZATION_LISTENERS;
+
+// {
+// this->panel = make_shared();
+// this->panel->addButton("Add...", [this]() {
+// auto nameString = ofSystemTextBoxDialog("Name");
+// if (!nameString.empty()) {
+//
+// auto heliostat = make_shared();
+// heliostat->name = nameString;
+// this->heliostats.add(heliostat);
+// }
+// })->setHeight(100.0f);
+// }
+ }
+
+ //----------
+ void Heliostats::populateInspector(ofxCvGui::InspectArguments & inspectArgs) {
+ auto inspector = inspectArgs.inspector;
+ this->heliostats.populateWidgets(inspector);
+ }
+
+ //----------
+ void Heliostats::serialize(Json::Value & json) {
+ this->heliostats.serialize(json["heliostats"]);
+ }
+
+ //----------
+ void Heliostats::deserialize(const Json::Value & json) {
+ this->heliostats.deserialize(json["heliostats"]);
+ }
+
+ //----------
+ ofxCvGui::PanelPtr Heliostats::getPanel() {
+ return this->panel;
+ }
+
+ //----------
+ void Heliostats::drawWorldStage() {
+ auto selection = this->heliostats.getSelection();
+ for (auto heliostat : selection) {
+ heliostat->drawWorld();
+ }
+ }
+
+ //----------
+ std::vector> Heliostats::getHeliostats() {
+ return this->heliostats.getSelection();
+ }
+
+ //----------
+ void Heliostats::add(shared_ptr heliostat) {
+ this->heliostats.add(heliostat);
+ }
+
+ //----------
+ void Heliostats::removeHeliostat(shared_ptr heliostat) {
+ this->heliostats.remove(heliostat);
+ }
+
+#pragma mark Heliostat
+ //----------
+ Heliostats::Heliostat::Heliostat() {
+ RULR_SERIALIZE_LISTENERS;
+ }
+
+ //----------
+ string Heliostats::Heliostat::getDisplayString() const {
+ stringstream ss;
+ ss << "Position : " << this->position << endl;
+ ss << "Rotation : " << this->rotationY;
+ return ss.str();
+ }
+
+ //----------
+ void Heliostats::Heliostat::drawWorld() {
+ ofPushMatrix();
+ {
+ ofTranslate(this->position);
+ ofRotate(this->rotationY.get(), 0, 1, 0);
+ ofDrawAxis(0.1f);
+ ofDrawBitmapString(this->name, ofVec3f());
+
+ ofTranslate(0.0f, -0.095f, 0.0f);
+ ofPushStyle();
+ {
+ ofNoFill();
+ ofSetColor(this->color);
+
+ static ofBoxPrimitive * box = 0;
+ if(!box) {
+ box = new ofBoxPrimitive();
+ box->setWidth(0.22f);
+ box->setHeight(0.29f);
+ box->setDepth(0.13f);
+ box->setResolution(1);
+ box->setUseVbo(true);
+ }
+ box->drawWireframe();
+ }
+ ofPopStyle();
+ }
+ ofPopMatrix();
+
+ auto captures = this->captures.getSelection();
+ for (auto capture : captures) {
+ capture->drawWorld();
+ }
+ }
+
+ //----------
+ void Heliostats::Heliostat::serialize(Json::Value & json) {
+ Utils::Serializable::serialize(json, this->name);
+ Utils::Serializable::serialize(json, this->position);
+ Utils::Serializable::serialize(json, this->rotationY);
+ Utils::Serializable::serialize(json, this->axis1Servo);
+ Utils::Serializable::serialize(json, this->axis2Servo);
+ this->captures.serialize(json["captures"]);
+ }
+
+ //----------
+ void Heliostats::Heliostat::deserialize(const Json::Value & json) {
+ Utils::Serializable::deserialize(json, this->name);
+ Utils::Serializable::deserialize(json, this->position);
+ Utils::Serializable::deserialize(json, this->rotationY);
+ Utils::Serializable::deserialize(json, this->axis1Servo);
+ Utils::Serializable::deserialize(json, this->axis2Servo);
+ this->captures.deserialize(json["captures"]);
+ }
+
+ //----------
+ void Heliostats::Heliostat::calcPosition(float axis2ToPlaneLength /*= 0.15f*/) {
+ auto captures = this->captures.getSelection();
+ if (!captures.empty()) {
+ ofVec3f accumulator;
+ for (auto capture : captures) {
+ auto backPosition = capture->mirrorPlane.meanWorldPoint
+ - capture->mirrorPlane.plane.getNormal()
+ * axis2ToPlaneLength;
+
+ accumulator += backPosition;
+ }
+ this->position = accumulator / captures.size();
+ }
+ }
+
+ //----------
+ ofxCvGui::ElementPtr Heliostats::Heliostat::getDataDisplay() {
+ auto element = ofxCvGui::makeElement();
+
+ auto widget0 = make_shared>(this->name);
+ {
+ element->addChild(widget0);
+ }
+
+ auto widget1 = make_shared>(this->position);
+ {
+ element->addChild(widget1);
+ }
+
+ auto widget2 = make_shared(this->rotationY);
+ {
+ element->addChild(widget2);
+ }
+
+ auto widget3 = make_shared>(this->axis1Servo);
+ {
+ element->addChild(widget3);
+ }
+
+ auto widget4 = make_shared>(this->axis2Servo);
+ {
+ element->addChild(widget4);
+ }
+
+ auto widget5 = make_shared>("Capture count : ", [this]() {
+ return this->captures.size();
+ });
+ {
+ element->addChild(widget5);
+ }
+
+ element->onBoundsChange += [this, widget0, widget1, widget2, widget3, widget4, widget5](ofxCvGui::BoundsChangeArguments & args) {
+ auto bounds = args.localBounds;
+ bounds.height = 40.0f;
+ widget0->setBounds(bounds);
+ bounds.y += bounds.height;
+ widget1->setBounds(bounds);
+ bounds.y += bounds.height;
+ widget2->setBounds(bounds);
+ bounds.y += bounds.height;
+ widget3->setBounds(bounds);
+ bounds.y += bounds.height;
+ widget4->setBounds(bounds);
+ bounds.y += bounds.height;
+ widget5->setBounds(bounds);
+ };
+
+ element->setHeight(240.0f);
+
+ return element;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/Heliostats.h b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/Heliostats.h
new file mode 100644
index 0000000000..28cf92c2b3
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/Heliostats.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "pch_Plugin_Experiments.h"
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace MirrorPlaneCapture {
+ class Heliostats : public Nodes::Base {
+ public:
+ class Heliostat : public Utils::AbstractCaptureSet::BaseCapture {
+ public:
+ Heliostat();
+ string getDisplayString() const override;
+ void drawWorld();
+
+ ofParameter name{ "Name", "" };
+ ofParameter position{ "Position", ofVec3f() };
+ ofParameter rotationY { "Rotation Y", 0.0f, -90, 90 };
+
+ ofParameter axis1Servo{ "Axis 1 Servo", 0 };
+ ofParameter axis2Servo{ "Axis 2 Servo", 0 };
+
+ void serialize(Json::Value &);
+ void deserialize(const Json::Value &);
+
+ Utils::CaptureSet captures;
+ void calcPosition(float axis2ToPlaneLength = 0.15f);
+
+ protected:
+ ofxCvGui::ElementPtr getDataDisplay() override;
+ };
+
+ Heliostats();
+ string getTypeName() const override;
+
+ void init();
+
+ void populateInspector(ofxCvGui::InspectArguments &);
+ void serialize(Json::Value &);
+ void deserialize(const Json::Value &);
+
+ ofxCvGui::PanelPtr getPanel() override;
+ void drawWorldStage();
+
+ vector> getHeliostats();
+ void add(shared_ptr);
+ void removeHeliostat(shared_ptr heliostat);
+ protected:
+ Utils::CaptureSet heliostats;
+ shared_ptr panel;
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/SolarAlignment.cpp b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/SolarAlignment.cpp
new file mode 100644
index 0000000000..4f2fd61b12
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/SolarAlignment.cpp
@@ -0,0 +1,440 @@
+#include "pch_Plugin_Experiments.h"
+
+float getMarkerReprojectionError2(vector imagePoints, cv::Mat cameraMatrix, cv::Mat distortionCoefficients) {
+ cv::Mat translation, rotationVector;
+
+ vector dummyObjectPoints;
+ {
+ dummyObjectPoints.emplace_back(0, 0, 0);
+ dummyObjectPoints.emplace_back(1, 0, 0);
+ dummyObjectPoints.emplace_back(1, 1, 0);
+ dummyObjectPoints.emplace_back(0, 1, 0);
+ }
+
+ cv::solvePnP(dummyObjectPoints
+ , imagePoints
+ , cameraMatrix
+ , distortionCoefficients
+ , rotationVector
+ , translation);
+
+ vector reprojectedPoints;
+ cv::projectPoints(dummyObjectPoints
+ , rotationVector
+ , translation
+ , cameraMatrix
+ , distortionCoefficients
+ , reprojectedPoints);
+
+ //get RMS
+ float accumulate = 0.0f;
+ for (int i = 0; i < imagePoints.size(); i++) {
+ auto delta = reprojectedPoints[i] - imagePoints[i];
+ accumulate += delta.dot(delta);
+ }
+ auto MS = accumulate / imagePoints.size();
+ return sqrt(MS);
+}
+
+//function to test reprojection error for a set of points given a translation, rotation
+float getReprojectionError2 (shared_ptr camera, const vector & objectPoints
+ , const vector & imagePoints
+ , const cv::Mat & rotationVector
+ , const cv::Mat & translation) {
+
+ float error = 0.0f;
+ vector reprojectedPoints;
+ cv::projectPoints(objectPoints
+ , rotationVector
+ , translation
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , reprojectedPoints);
+
+ //Take square mean
+ for (size_t i = 0; i < imagePoints.size(); i++) {
+ auto delta = imagePoints[i] - reprojectedPoints[i];
+ error += delta.x * delta.x + delta.y * delta.y;
+ }
+ //RMS
+ error /= (float)imagePoints.size();
+ error = sqrt(error);
+
+ return error;
+};
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace MirrorPlaneCapture {
+#pragma mark Captures
+ //----------
+ SolarAlignment::SolarAlignment() {
+ RULR_NODE_INIT_LISTENER;
+ }
+
+ //----------
+ std::string SolarAlignment::getTypeName() const {
+ return "HALO::SolarAlignment";
+ }
+
+ //----------
+ void SolarAlignment::init() {
+ RULR_NODE_INSPECTOR_LISTENER;
+ RULR_NODE_SERIALIZATION_LISTENERS;
+ RULR_NODE_DRAW_WORLD_LISTENER;
+
+ {
+ auto panel = ofxCvGui::Panels::makeImage(this->preview);
+ panel->onDrawImage += [this](ofxCvGui::DrawImageArguments & args) {
+ auto captures = this->captures.getSelection();
+ for (auto capture : captures) {
+ capture->draw();
+ }
+ };
+ this->panel = panel;
+ }
+
+ this->addInput();
+ this->addInput();
+
+ this->manageParameters(this->parameters);
+ }
+
+ //----------
+ void SolarAlignment::populateInspector(ofxCvGui::InspectArguments & inspectArgs) {
+ auto inspector = inspectArgs.inspector;
+ this->captures.populateWidgets(inspector);
+ inspector->addButton("Add capture", [this]() {
+ try {
+ Utils::ScopedProcess scopedProcess("Add capture");
+ this->addCapture();
+ scopedProcess.end();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ }, ' ');
+ inspector->addButton("Calibrate", [this]() {
+ try {
+ Utils::ScopedProcess scopedProcess("Calibrate");
+ this->calibrate();
+ scopedProcess.end();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ }, OF_KEY_RETURN)->setHeight(100.0f);
+ }
+
+ //----------
+ void SolarAlignment::serialize(Json::Value & json) {
+ this->captures.serialize(json["captures"]);
+ }
+
+ //----------
+ void SolarAlignment::deserialize(const Json::Value & json) {
+ this->captures.deserialize(json["captures"]);
+ }
+
+ //----------
+ ofxCvGui::PanelPtr SolarAlignment::getPanel() {
+ return this->panel;
+ }
+
+ //----------
+ void SolarAlignment::drawWorldStage() {
+ auto captures = this->captures.getSelection();
+ for (auto capture : captures) {
+ capture->drawWorld();
+ }
+ }
+
+ //----------
+ void SolarAlignment::addCapture() {
+ this->throwIfMissingAnyConnection();
+ auto camera = this->getInput();
+
+ auto capture = make_shared();
+
+ //navigate the camera
+ {
+ Utils::ScopedProcess scopedProcess("Marker map navigation image", false);
+ auto markerMap = this->getInput();
+ markerMap->throwIfMissingAConnection();
+ auto detector = markerMap->getInput();
+
+ auto frame = camera->getFreshFrame();
+ cv::Mat image = ofxCv::toCv(frame->getPixels());
+
+ auto markers = detector->findMarkers(image);
+ if (markers.size() < this->parameters.cameraNavigation.minimumMarkers) {
+ throw(ofxRulr::Exception("Found " + ofToString(markers.size()) + " real markers. Need " + ofToString(this->parameters.cameraNavigation.minimumMarkers) + " for camera navigation"));
+ }
+
+ //build up dataset
+ vector worldPoints;
+ vector imagePoints;
+ {
+ int countGoodMarkers = 0;
+ for (const auto & marker : markers) {
+ try {
+ //test reprojection error
+ auto markerReprojectionError = getMarkerReprojectionError2((vector &) marker
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients());
+
+ if (markerReprojectionError <= this->parameters.cameraNavigation.maxMarkerError) {
+ countGoodMarkers++;
+ auto marker3D = markerMap->getMarkerMap()->getMarker3DInfo(marker.id);
+ for (int i = 0; i < 4; i++) {
+ worldPoints.push_back(marker3D.points[i]);
+ imagePoints.push_back(marker[i]);
+ }
+ }
+ }
+ RULR_CATCH_ALL_TO_ERROR; //in the case that the marker did not exist
+ }
+
+ if (countGoodMarkers < this->parameters.cameraNavigation.minimumMarkers) {
+ throw(ofxRulr::Exception("Found " + ofToString(countGoodMarkers) + " good markers in map. Need " + ofToString(this->parameters.cameraNavigation.minimumMarkers) + " for camera navigation"));
+ }
+ }
+
+ cv::Mat rotationVector, translation;
+ camera->getExtrinsics(rotationVector, translation, true);
+
+ //Solve camera extrinsics
+ {
+ //first pass EPNP
+ cv::solvePnPRansac(worldPoints
+ , imagePoints
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , rotationVector
+ , translation
+ , true
+ , 100
+ , 5.0f
+ , 0.99
+ , cv::noArray()
+ , cv::SOLVEPNP_EPNP);
+
+ //second pass ITERATIVE
+ cv::solvePnPRansac(worldPoints
+ , imagePoints
+ , camera->getCameraMatrix()
+ , camera->getDistortionCoefficients()
+ , rotationVector
+ , translation
+ , true
+ , 100
+ , 5.0f
+ , 0.99
+ , cv::noArray()
+ , cv::SOLVEPNP_ITERATIVE);
+ }
+
+ camera->setExtrinsics(rotationVector, translation, true);
+
+ capture->cameraNavigation.imagePoints = imagePoints;
+ capture->cameraNavigation.worldPoints = worldPoints;
+ capture->cameraNavigation.reprojectionError = getReprojectionError2(camera
+ , worldPoints
+ , imagePoints
+ , rotationVector
+ , translation);
+ capture->cameraNavigation.cameraPosition = camera->getPosition();
+ }
+
+ //get the solar vector
+ {
+ Utils::ScopedProcess scopedProcess("Solar centroid image", false);
+ auto frame = camera->getFreshFrame();
+ cv::Mat image = ofxCv::toCv(frame->getPixels());
+
+ if (image.channels() == 3) {
+ cv::cvtColor(image, image, CV_RGB2GRAY);
+ }
+
+ cv::Mat binary;
+
+ cv::threshold(image
+ , binary
+ , this->parameters.solarCentroid.threshold
+ , 255
+ , CV_THRESH_BINARY);
+
+ vector> contours;
+ vector centroids;
+ vector boundingRects;
+ vector moments;
+
+ cv::findContours(binary
+ , contours
+ , CV_RETR_EXTERNAL
+ , CV_CHAIN_APPROX_NONE);
+
+ if (contours.size() < 1) {
+ throw(ofxRulr::Exception("Found 0 contours. Need 1"));
+ }
+
+ vector bestContour;
+ float bestArea = 0;
+ for (const auto & contour : contours) {
+ auto bounds = cv::boundingRect(contour);
+ auto area = bounds.area();
+ if (area > bestArea) {
+ bestContour = contour;
+ bestArea = area;
+ }
+ }
+
+ cv::Rect2i dilatedRect = cv::boundingRect(bestContour);
+ {
+ dilatedRect.x -= 5;
+ dilatedRect.y -= 5;
+ dilatedRect.width += 5;
+ dilatedRect.height += 5;
+ }
+
+ auto moment = cv::moments(image(dilatedRect));
+ capture->centroid = ofVec2f(moment.m10 / moment.m00 + dilatedRect.x
+ , moment.m01 / moment.m00 + dilatedRect.y);
+
+ auto cameraView = camera->getViewInWorldSpace();
+
+ capture->ray = cameraView.castPixel(capture->centroid);
+
+ cv::Mat preview;
+ image.copyTo(preview, binary);
+ ofxCv::copy(preview, this->preview);
+ this->preview.update();
+ }
+
+ //get the timestamp of the file
+ {
+ auto result = ofSystemTextBoxDialog("Epoch timestamp for capture");
+ if (result.empty()) {
+ return;
+ }
+
+ time_t epochTime = ofToInt(result);
+ capture->timestamp.set(chrono::system_clock::from_time_t(epochTime));
+ capture->rebuildDateStrings();
+ }
+
+ //get the solar tracking for this time
+ {
+ auto result = ofSystemTextBoxDialog("Azimuth, Altitude from sunPosition.py");
+ if (result.empty()) {
+ return;
+ }
+
+ stringstream ss(result);
+ ss >> capture->azimuthAltitude;
+ }
+
+ //calculate the solar vector in global frame
+ {
+ auto azRotate = ofMatrix4x4::newRotationMatrix(capture->azimuthAltitude.x, 0, 1, 0);
+ auto elRotate = ofMatrix4x4::newRotationMatrix(capture->azimuthAltitude.y, +1, 0, 0);
+ capture->solarVectorFromPySolar = azRotate * elRotate * ofVec3f(0, 0, -1);
+ }
+
+ this->captures.add(capture);
+ }
+
+ //----------
+ void SolarAlignment::calibrate() {
+
+ }
+
+#pragma mark Capture
+ //----------
+ SolarAlignment::Capture::Capture() {
+ RULR_SERIALIZE_LISTENERS;
+ }
+
+ //----------
+ std::string SolarAlignment::Capture::getDisplayString() const {
+ stringstream ss;
+ ss << "Solar vector : " << this->ray.t << endl;
+ ss << "Azi/Alt : " << this->azimuthAltitude << endl;
+ return ss.str();
+ }
+
+ //----------
+ void SolarAlignment::Capture::drawWorld() {
+ this->ray.color = this->color;
+ this->ray.draw();
+
+ ofPushStyle();
+ {
+ ofPushMatrix();
+ {
+ ofSetColor(this->color);
+ ofRotate(this->azimuthAltitude.x, 0, +1, 0);
+ ofRotate(this->azimuthAltitude.y, +1, 0, 0);
+ ofDrawLine(ofVec3f(), ofVec3f(0, 0, -10));
+ }
+ ofPopMatrix();
+
+ ofPushMatrix();
+ {
+ ofTranslate(0.1, 0.0, 0.0);
+ ofDrawLine(ofVec3f(), this->solarVectorFromPySolar);
+ }
+ ofPopMatrix();
+ }
+ ofPopStyle();
+ }
+
+ //----------
+ void SolarAlignment::Capture::serialize(Json::Value & json) {
+ json["ray_s"] << this->ray.s;
+ json["ray_t"] << this->ray.t;
+ json["centroid"] << this->centroid;
+ json["azimuthAltitude"] << this->azimuthAltitude;
+
+ {
+ auto & jsonCameraNavigation = json["cameraNavigation"];
+ jsonCameraNavigation["imagePoints"] << ofxCv::toOf(this->cameraNavigation.imagePoints);
+ jsonCameraNavigation["worldPoints"] << ofxCv::toOf(this->cameraNavigation.worldPoints);
+ jsonCameraNavigation["cameraPosition"] << this->cameraNavigation.cameraPosition;
+ Utils::Serializable::serialize(jsonCameraNavigation, this->cameraNavigation.reprojectionError);
+ }
+ }
+
+ //----------
+ void SolarAlignment::Capture::deserialize(const Json::Value & json) {
+ json["ray_s"] >> this->ray.s;
+ json["ray_t"] >> this->ray.t;
+ this->ray.defined = true;
+ json["centroid"] >> this->centroid;
+ json["azimuthAltitude"] >> this->azimuthAltitude;
+
+ {
+ const auto & jsonCameraNavigation = json["cameraNavigation"];
+ jsonCameraNavigation["imagePoints"] >> ofxCv::toOf(this->cameraNavigation.imagePoints);
+ jsonCameraNavigation["worldPoints"] >> ofxCv::toOf(this->cameraNavigation.worldPoints);
+ jsonCameraNavigation["cameraPosition"] >> this->cameraNavigation.cameraPosition;
+ Utils::Serializable::deserialize(jsonCameraNavigation, this->cameraNavigation.reprojectionError);
+ }
+ }
+
+ //----------
+ void SolarAlignment::Capture::draw() {
+ ofPushStyle();
+ {
+ ofSetColor(this->color);
+ ofPushMatrix();
+ {
+ ofTranslate(this->centroid);
+ ofDrawLine(-10, 0, 10, 0);
+ ofDrawLine(0, -10, 0, 10);
+ }
+ ofPopMatrix();
+ }
+ ofPopStyle();
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/SolarAlignment.h b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/SolarAlignment.h
new file mode 100644
index 0000000000..5dd52d8975
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/SolarAlignment.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include "pch_Plugin_Experiments.h"
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace MirrorPlaneCapture {
+ class SolarAlignment : public Nodes::Base {
+ public:
+ class Capture : public Utils::AbstractCaptureSet::BaseCapture {
+ public:
+ Capture();
+ string getDisplayString() const override;
+ void drawWorld();
+
+ ofVec2f centroid;
+ ofxRay::Ray ray;
+ ofVec2f azimuthAltitude;
+ ofVec3f solarVectorFromPySolar;
+
+ void serialize(Json::Value &);
+ void deserialize(const Json::Value &);
+
+ void draw();
+
+ struct {
+ vector imagePoints;
+ vector worldPoints;
+ ofParameter reprojectionError{ "Marker map reprojection error", 0.0f };
+ ofParameter cameraPosition{ "Camera position", ofVec3f() };
+ } cameraNavigation;
+ };
+
+ SolarAlignment();
+ string getTypeName() const override;
+
+ void init();
+
+ void populateInspector(ofxCvGui::InspectArguments &);
+ void serialize(Json::Value &);
+ void deserialize(const Json::Value &);
+
+ ofxCvGui::PanelPtr getPanel() override;
+ void drawWorldStage();
+
+ void addCapture();
+ void calibrate();
+ protected:
+ struct : ofParameterGroup {
+ struct : ofParameterGroup {
+ ofParameter minimumMarkers{ "Minimum markers", 3 };
+ ofParameter maxMarkerError{ "Max marker error", 0.5f };
+ PARAM_DECLARE("Camera navigation", minimumMarkers, maxMarkerError);
+ } cameraNavigation;
+
+ struct : ofParameterGroup {
+ ofParameter threshold{ "Threshold", 100, 0, 255 };
+ PARAM_DECLARE("Solar centroid", threshold);
+ } solarCentroid;
+
+ struct : ofParameterGroup {
+ ofParameter sunTrackingScript{ "sunPosition.pyv", "C:\\Users\\kimchips\\Dropbox (Kimchi and Chips)\\KC31 - Halo v2.0 Technical\\Sun Tracking\\sunPosition.py" };
+ ofParameter python{ "python exe", "C:\\Users\\kimchips\\Anaconda3\\envs\\KC31\\python.exe" };
+ PARAM_DECLARE("Solar tracking", sunTrackingScript, python);
+ } solarTracking;
+
+ PARAM_DECLARE("SolarAlignment", cameraNavigation, solarCentroid, solarTracking);
+ } parameters;
+
+ Utils::CaptureSet captures;
+ shared_ptr panel;
+ ofImage preview;
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/planesToPoint.cpp b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/planesToPoint.cpp
new file mode 100644
index 0000000000..f20a5657fa
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/planesToPoint.cpp
@@ -0,0 +1,38 @@
+#include "pch_Plugin_Experiments.h"
+#include "planesToPoint.h"
+
+class Model : public ofxNonLinearFit::Models::Base {
+public:
+ unsigned int getParameterCount() const {
+ return 3;
+ }
+
+ void getResidual(ofxRay::Plane plane, double & residual, double * gradient = 0) const {
+ if (gradient) {
+ throw(ofxRulr::Exception("not implemented"));
+ }
+
+ residual = plane.getDistanceTo(this->point);
+ };
+
+ void evaluate(ofxRay::Plane &) const {
+ //nothing to do
+ }
+
+ void cacheModel() {
+ this->point.x = this->parameters[0];
+ this->point.y = this->parameters[1];
+ this->point.z = this->parameters[2];
+ }
+
+ ofVec3f point;
+};
+
+ofVec3f planesToPoint(const vector & planes) {
+ ofxNonLinearFit::Fit fit;
+
+ Model model;
+ double residual;
+ fit.optimise(model, &planes, &residual);
+ return model.point;
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/planesToPoint.h b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/planesToPoint.h
new file mode 100644
index 0000000000..adc375e2e2
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/MirrorPlaneCapture/planesToPoint.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "pch_Plugin_Experiments.h"
+
+ofVec3f planesToPoint(const vector & planes);
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/PhotoScan/BundlerCamera.cpp b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/PhotoScan/BundlerCamera.cpp
new file mode 100644
index 0000000000..e30f6f64ba
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/PhotoScan/BundlerCamera.cpp
@@ -0,0 +1,196 @@
+#include "pch_Plugin_Experiments.h"
+#include "BundlerCamera.h"
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace PhotoScan {
+ //----------
+ void BundlerCamera::Capture::drawWorld() {
+ this->preview.drawVertices();
+ }
+
+ //----------
+ string BundlerCamera::Capture::getDisplayString() const {
+ stringstream ss;
+ ss << "Tie points : " << this->worldPoints.size();
+ return ss.str();
+ }
+
+ //----------
+ BundlerCamera::BundlerCamera() {
+ RULR_NODE_INIT_LISTENER;
+ }
+
+ //----------
+ string BundlerCamera::getTypeName() const {
+ return "Experiments::PhotoScan::BundlerCamera";
+ }
+
+ //----------
+ void BundlerCamera::init() {
+ RULR_NODE_INSPECTOR_LISTENER;
+ RULR_NODE_DRAW_WORLD_LISTENER;
+
+ this->addInput();
+
+ this->panel = ofxCvGui::Panels::makeImage(this->previewTiePoints);
+
+ this->manageParameters(this->parameters);
+ }
+
+ //----------
+ void BundlerCamera::addCapture() {
+
+ }
+
+ //----------
+ void BundlerCamera::calibrate() {
+ this->throwIfMissingAnyConnection();
+ auto camera = this->getInput();
+
+ auto captures = this->captures.getSelection();
+ if (captures.size() != 1) {
+ throw(ofxRulr::Exception("Please select exactly one capture when calibrating camera"));
+ }
+
+ auto capture = captures[0];
+
+ vector> worldPointsSets(1);
+ vector> imagePointsSets(1);
+ {
+ const auto cameraWidth = camera->getWidth();
+ const auto cameraHeight = camera->getHeight();
+
+ //clear preview
+ this->previewTiePoints.allocate(cameraWidth, cameraHeight, OF_IMAGE_COLOR_ALPHA);
+ this->previewTiePoints.getPixels().set(0);
+ auto previewTiePointsPixels = this->previewTiePoints.getPixels().getData();
+
+ for (int i = 0; i < capture->imagePoints.size(); i+= this->parameters.calibrate.decimator) {
+ const auto & rawWorldPoint = capture->worldPoints[i];
+ const auto & rawImagePoint = capture->imagePoints[i];
+
+ auto imagePoint = cv::Point2f(cameraWidth / 2.0f + rawImagePoint.x
+ , cameraHeight / 2.0f - rawImagePoint.y
+ );
+
+ worldPointsSets[0].push_back(rawWorldPoint);
+ imagePointsSets[0].push_back(imagePoint);
+
+ //colour the preview pixel
+ int pixelIndex = imagePoint.x + imagePoint.y * cameraWidth;
+ previewTiePointsPixels[pixelIndex] = 255;
+ }
+
+ this->previewTiePoints.update();
+ }
+
+ cv::Mat cameraMatrix = camera->getCameraMatrix();
+ cv::Mat distortionCoefficients = camera->getDistortionCoefficients();
+ vector rotationVectors, translations;
+
+ this->reprojectionError = cv::calibrateCamera(worldPointsSets
+ , imagePointsSets
+ , camera->getSize()
+ , cameraMatrix
+ , distortionCoefficients
+ , rotationVectors
+ , translations
+ , CV_CALIB_USE_INTRINSIC_GUESS | CV_CALIB_FIX_K5 | CV_CALIB_FIX_K6);
+
+ camera->setIntrinsics(cameraMatrix, distortionCoefficients);
+ camera->setExtrinsics(rotationVectors[0]
+ , translations[0]
+ , true);
+ }
+
+ //----------
+ ofxCvGui::PanelPtr BundlerCamera::getPanel() {
+ return this->panel;
+ }
+
+ //----------
+ void BundlerCamera::populateInspector(ofxCvGui::InspectArguments & args) {
+ auto inspector = args.inspector;
+
+ inspector->addButton("Import bundler...", [this]() {
+ try {
+ Utils::ScopedProcess scopedProcess("Loading bundler file");
+
+ auto fileResult = ofSystemLoadDialog("Select bundler_formatted.json file");
+ if (!fileResult.bSuccess) {
+ return;
+ }
+
+ this->importBundler(fileResult.filePath);
+ scopedProcess.end();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ });
+
+ this->captures.populateWidgets(inspector);
+
+ inspector->addButton("Calibrate", [this]() {
+ try {
+ this->calibrate();
+ }
+ RULR_CATCH_ALL_TO_ALERT;
+ }, OF_KEY_RETURN)->setHeight(100.0f);
+
+ inspector->addLiveValue(this->reprojectionError);
+ }
+
+ //----------
+ void BundlerCamera::drawWorldStage() {
+ auto captures = this->captures.getSelection();
+ for (auto capture : captures) {
+ capture->drawWorld();
+ }
+ }
+
+ //----------
+ void BundlerCamera::importBundler(const string & filePath) {
+ auto fileContents = ofFile(filePath).readToBuffer();
+ Json::Value json;
+ Json::Reader().parse((const string &)fileContents, json);
+
+ for (const auto & jsonView : json) {
+ auto capture = make_shared();
+ capture->f = jsonView["f"].asFloat();
+ capture->k1 = jsonView["k1"].asFloat();
+ capture->k2 = jsonView["k2"].asFloat();
+
+ const auto & jsonTiePoints = jsonView["tiePoints"];
+ for (const auto & jsonTiePoint : jsonTiePoints) {
+ ofVec3f worldPoint;
+ for (int i = 0; i < 3; i++) {
+ worldPoint[i] = jsonTiePoint["worldSpace"][i].asFloat();
+ }
+
+ ofVec2f imagePoint;
+ for (int i = 0; i < 2; i++) {
+ imagePoint[i] = jsonTiePoint["imageSpace"][i].asFloat();
+ }
+
+ ofFloatColor color(255, 255, 255, 255);
+ for (int i = 0; i < 3; i++) {
+ color[i] = jsonTiePoint["color"][i].asFloat();
+ }
+ color /= 255;
+
+ capture->worldPoints.push_back(ofxCv::toCv(worldPoint));
+ capture->imagePoints.push_back(ofxCv::toCv(imagePoint));
+ capture->tiePointColors.push_back(color);
+ }
+
+ capture->preview.addVertices(ofxCv::toOf(capture->worldPoints));
+ capture->preview.addColors(capture->tiePointColors);
+
+ this->captures.add(capture);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/PhotoScan/BundlerCamera.h b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/PhotoScan/BundlerCamera.h
new file mode 100644
index 0000000000..d2ad04fc91
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/PhotoScan/BundlerCamera.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "ofxRulr.h"
+#include "ofxNonLinearFit.h"
+#include "ofxRulr/Utils/CaptureSet.h"
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace PhotoScan {
+ class BundlerCamera : public Base {
+ public:
+ class Capture : public Utils::AbstractCaptureSet::BaseCapture {
+ public:
+ void drawWorld();
+ string getDisplayString() const override;
+
+ ofParameter f{ "f", 0.0f };
+ ofParameter k1{ "k1", 0.0f };
+ ofParameter k2{ "k2", 0.0f };
+
+ vector worldPoints;
+ vector imagePoints;
+ vector tiePointColors;
+
+ ofMesh preview;
+ };
+
+ BundlerCamera();
+ string getTypeName() const override;
+ void init();
+ void addCapture();
+ void calibrate();
+ ofxCvGui::PanelPtr getPanel() override;
+ void populateInspector(ofxCvGui::InspectArguments &);
+ void drawWorldStage();
+
+ void importBundler(const string & filePath);
+ protected:
+ struct : ofParameterGroup {
+ struct : ofParameterGroup {
+ ofParameter decimator{ "Decimator", 1, 1, 256 };
+ PARAM_DECLARE("Calibrate", decimator);
+ } calibrate;
+
+ PARAM_DECLARE("BundlerCamera", calibrate);
+ } parameters;
+
+ ofxCvGui::PanelPtr panel;
+
+ Utils::CaptureSet captures;
+
+ ofParameter reprojectionError{ "Reprojection error [px]", 0, 0, 100.0 };
+
+ ofImage previewTiePoints;
+ };
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/SolveMirror/SolveMirror.cpp b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/SolveMirror/SolveMirror.cpp
new file mode 100644
index 0000000000..9a0aedff25
--- /dev/null
+++ b/Plugin_Experiments/src/ofxRulr/Nodes/Experiments/SolveMirror/SolveMirror.cpp
@@ -0,0 +1,350 @@
+#include "pch_Plugin_Experiments.h"
+
+namespace ofxRulr {
+ namespace Nodes {
+ namespace Experiments {
+ namespace SolveMirror {
+ //----------
+ SolveMirror::SolveMirror() {
+ RULR_NODE_INIT_LISTENER;
+ }
+
+ //----------
+ string SolveMirror::getTypeName() const {
+ return "Experiments::SolveMirror::SolveMirror";
+ }
+
+ //----------
+ void SolveMirror::init() {
+ RULR_NODE_DRAW_WORLD_LISTENER;
+ RULR_NODE_INSPECTOR_LISTENER;
+ RULR_NODE_UPDATE_LISTENER;
+
+ this->addInput();
+ this->addInput();
+ this->addInput();
+
+ this->panel = make_shared();
+ this->captures.populateWidgets(this->panel);
+ }
+
+ //----------
+ void SolveMirror::update() {
+
+ }
+
+ //----------
+ void SolveMirror::addCapture() {
+ this->throwIfMissingAnyConnection();
+ auto camera = this->getInput();
+ auto grabber = camera->getGrabber();
+ if (!grabber) {
+ throw(ofxRulr::Exception("Camera grabber not available"));
+ }
+ auto frame = grabber->getFreshFrame();
+ auto image = ofxCv::toCv(frame->getPixels());
+ auto detector = this->getInput();
+ auto markerMap = this->getInput()->getMarkerMap();
+
+ auto capture = make_shared();
+
+ auto getReprojectionError = [& camera](const vector & objectPoints
+ , const vector