From 2ef39de72fbd6833f71756498a26c2891500776f Mon Sep 17 00:00:00 2001 From: El Hadi Moussi Date: Mon, 5 Aug 2024 11:34:26 +0200 Subject: [PATCH] Reference version of Shape Recognition --- CMakeLists.txt | 20 + resources/CMakeLists.txt | 5 + resources/ShapeRecognCone.med | Bin 0 -> 125983 bytes resources/ShapeRecognCylinder.med | Bin 0 -> 70663 bytes resources/ShapeRecognPlane.med | Bin 0 -> 12832 bytes resources/ShapeRecognSphere.med | Bin 0 -> 25775 bytes resources/ShapeRecognTorus.med | Bin 0 -> 43247 bytes src/CMakeLists.txt | 4 + src/ShapeRecogn/Areas.cxx | 485 ++++++++++++++++++ src/ShapeRecogn/Areas.hxx | 108 ++++ src/ShapeRecogn/AreasBuilder.cxx | 371 ++++++++++++++ src/ShapeRecogn/AreasBuilder.hxx | 74 +++ src/ShapeRecogn/CMakeLists.txt | 63 +++ src/ShapeRecogn/MathOps.cxx | 331 ++++++++++++ src/ShapeRecogn/MathOps.hxx | 71 +++ src/ShapeRecogn/Nodes.cxx | 125 +++++ src/ShapeRecogn/Nodes.hxx | 78 +++ src/ShapeRecogn/NodesBuilder.cxx | 311 +++++++++++ src/ShapeRecogn/NodesBuilder.hxx | 62 +++ src/ShapeRecogn/PrimitiveType.hxx | 93 ++++ src/ShapeRecogn/README.md | 106 ++++ src/ShapeRecogn/ShapeRecognMesh.cxx | 155 ++++++ src/ShapeRecogn/ShapeRecognMesh.hxx | 80 +++ src/ShapeRecogn/ShapeRecognMeshBuilder.cxx | 250 +++++++++ src/ShapeRecogn/ShapeRecognMeshBuilder.hxx | 85 +++ src/ShapeRecogn/ShapeRecongConstants.hxx | 43 ++ src/ShapeRecogn/Swig/CMakeLists.txt | 83 +++ src/ShapeRecogn/Swig/ShapeRecogn.i | 15 + src/ShapeRecogn/Test/CMakeLists.txt | 65 +++ .../Test/CTestTestfileInstall.cmake | 31 ++ src/ShapeRecogn/Test/ConeTest.cxx | 324 ++++++++++++ src/ShapeRecogn/Test/ConeTest.hxx | 62 +++ src/ShapeRecogn/Test/CylinderTest.cxx | 136 +++++ src/ShapeRecogn/Test/CylinderTest.hxx | 55 ++ src/ShapeRecogn/Test/MathOpsTest.cxx | 97 ++++ src/ShapeRecogn/Test/MathOpsTest.hxx | 51 ++ src/ShapeRecogn/Test/PlaneTest.cxx | 44 ++ src/ShapeRecogn/Test/PlaneTest.hxx | 49 ++ src/ShapeRecogn/Test/SphereTest.cxx | 36 ++ src/ShapeRecogn/Test/SphereTest.hxx | 49 ++ src/ShapeRecogn/Test/TestShapeRecogn.cxx | 34 ++ src/ShapeRecogn/Test/TorusTest.cxx | 38 ++ src/ShapeRecogn/Test/TorusTest.hxx | 49 ++ 43 files changed, 4138 insertions(+) create mode 100644 resources/ShapeRecognCone.med create mode 100755 resources/ShapeRecognCylinder.med create mode 100755 resources/ShapeRecognPlane.med create mode 100755 resources/ShapeRecognSphere.med create mode 100755 resources/ShapeRecognTorus.med create mode 100644 src/ShapeRecogn/Areas.cxx create mode 100644 src/ShapeRecogn/Areas.hxx create mode 100644 src/ShapeRecogn/AreasBuilder.cxx create mode 100644 src/ShapeRecogn/AreasBuilder.hxx create mode 100644 src/ShapeRecogn/CMakeLists.txt create mode 100644 src/ShapeRecogn/MathOps.cxx create mode 100644 src/ShapeRecogn/MathOps.hxx create mode 100644 src/ShapeRecogn/Nodes.cxx create mode 100644 src/ShapeRecogn/Nodes.hxx create mode 100644 src/ShapeRecogn/NodesBuilder.cxx create mode 100644 src/ShapeRecogn/NodesBuilder.hxx create mode 100644 src/ShapeRecogn/PrimitiveType.hxx create mode 100644 src/ShapeRecogn/README.md create mode 100644 src/ShapeRecogn/ShapeRecognMesh.cxx create mode 100644 src/ShapeRecogn/ShapeRecognMesh.hxx create mode 100644 src/ShapeRecogn/ShapeRecognMeshBuilder.cxx create mode 100644 src/ShapeRecogn/ShapeRecognMeshBuilder.hxx create mode 100644 src/ShapeRecogn/ShapeRecongConstants.hxx create mode 100644 src/ShapeRecogn/Swig/CMakeLists.txt create mode 100644 src/ShapeRecogn/Swig/ShapeRecogn.i create mode 100644 src/ShapeRecogn/Test/CMakeLists.txt create mode 100644 src/ShapeRecogn/Test/CTestTestfileInstall.cmake create mode 100644 src/ShapeRecogn/Test/ConeTest.cxx create mode 100644 src/ShapeRecogn/Test/ConeTest.hxx create mode 100644 src/ShapeRecogn/Test/CylinderTest.cxx create mode 100644 src/ShapeRecogn/Test/CylinderTest.hxx create mode 100644 src/ShapeRecogn/Test/MathOpsTest.cxx create mode 100644 src/ShapeRecogn/Test/MathOpsTest.hxx create mode 100644 src/ShapeRecogn/Test/PlaneTest.cxx create mode 100644 src/ShapeRecogn/Test/PlaneTest.hxx create mode 100644 src/ShapeRecogn/Test/SphereTest.cxx create mode 100644 src/ShapeRecogn/Test/SphereTest.hxx create mode 100644 src/ShapeRecogn/Test/TestShapeRecogn.cxx create mode 100644 src/ShapeRecogn/Test/TorusTest.cxx create mode 100644 src/ShapeRecogn/Test/TorusTest.hxx diff --git a/CMakeLists.txt b/CMakeLists.txt index 840bac26a..c080f174a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,7 @@ OPTION(MEDCOUPLING_MICROMED "Build MED without MED file dependency." OFF) OPTION(MEDCOUPLING_ENABLE_PYTHON "Build PYTHON bindings." ON) OPTION(MEDCOUPLING_ENABLE_PARTITIONER "Build MEDPartitioner." ON) OPTION(MEDCOUPLING_ENABLE_RENUMBER "Build Renumber." ON) +OPTION(MEDCOUPLING_ENABLE_SHAPERECOGN "Build ShapeRecogn" OFF) OPTION(MEDCOUPLING_WITH_FILE_EXAMPLES "Install examples of files containing meshes and fields of different formats." ON) OPTION(MEDCOUPLING_USE_MPI "(Use MPI containers) - For MED this triggers the build of ParaMEDMEM." OFF) OPTION(MEDCOUPLING_BUILD_TESTS "Build MEDCoupling C++ tests." ON) @@ -201,6 +202,21 @@ IF(MEDCOUPLING_ENABLE_RENUMBER) SALOME_LOG_OPTIONAL_PACKAGE(Boost MEDCOUPLING_ENABLE_RENUMBER) ENDIF(MEDCOUPLING_ENABLE_RENUMBER) +IF(MEDCOUPLING_ENABLE_SHAPERECOGN) + FIND_PACKAGE(BLAS REQUIRED) + FIND_PACKAGE(LAPACK REQUIRED) + FIND_LIBRARY(LAPACKE_LIB NAMES lapacke REQUIRED) + SET(LAPACK_LIBRARIES ${LAPACKE_LIB} ${LAPACK_LIBRARIES} ${BLAS_LIBRARIES}) + FIND_PATH(LAPACKE_INCLUDE_DIRS NAMES lapacke.h HINTS ${LAPACK_LIBRARIES}) + IF(LAPACK_FOUND) + MESSAGE(STATUS "Lapacke libraries: ${LAPACK_LIBRARIES}") + MESSAGE(STATUS "Lapacke include dirs: ${LAPACKE_INCLUDE_DIRS}") + ELSE() + MESSAGE(FATAL_ERROR "Error in Lapacke detection ! lapacke not found !") + ENDIF(LAPACK_FOUND) + SALOME_LOG_OPTIONAL_PACKAGE(Lapack MEDCOUPLING_ENABLE_SHAPERECOGN) +ENDIF(MEDCOUPLING_ENABLE_SHAPERECOGN) + IF(MEDCOUPLING_ENABLE_PYTHON) FIND_PACKAGE(SalomePythonInterp) FIND_PACKAGE(SalomePythonLibs) @@ -339,6 +355,10 @@ IF(MEDCOUPLING_USE_MPI) ENDIF() ENDIF() ENDIF() + +IF(MEDCOUPLING_ENABLE_SHAPERECOGN) + LIST(APPEND _${PROJECT_NAME}_exposed_targets shaperecogn) +ENDIF(MEDCOUPLING_ENABLE_SHAPERECOGN) # Add all targets to the build-tree export set diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt index 704221ca6..cf6ec2481 100644 --- a/resources/CMakeLists.txt +++ b/resources/CMakeLists.txt @@ -94,6 +94,11 @@ SET(MED_other_FILES test_MED_MAIL.sauv castem17_result_xdr.sauv castem17_result_ascii.sauv + ShapeRecognPlane.med + ShapeRecognCylinder.med + ShapeRecognCone.med + ShapeRecognSphere.med + ShapeRecognTorus.med ) SET(MED_RESOURCES_FILES ${MED_test_fig_files}) diff --git a/resources/ShapeRecognCone.med b/resources/ShapeRecognCone.med new file mode 100644 index 0000000000000000000000000000000000000000..da32d3e8b47703ce085a1d5fb212c191b2f54ae7 GIT binary patch literal 125983 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 VPHp@Tx0p`3)gp^3@V}t~{{`RP@TLF& literal 0 HcmV?d00001 diff --git a/resources/ShapeRecognCylinder.med b/resources/ShapeRecognCylinder.med new file mode 100755 index 0000000000000000000000000000000000000000..f028da1ff47714c7dad67ac35556c9918894a9f7 GIT binary patch literal 70663 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

w8#2|m zBU60`GSzn?Q#}fq>bsDsz8jh9dyuKV7n$nO$W-5lO!XLKs_#dp`T=CBA4I15A>>#{ z%daETIG}M$<18M_>H6J3rsYSFX}>hiX}x1uPW!onOyh>;!D-ZK{RCv%-x*{YcQg)Z zTnboz8Pfh}Uef-Ou{;G*zN%8chD^t$`9SA=5z9|OIu7;cSJXekTaemM_qQ_CX};V< zE`>CI=r|itC#gRkpsw04G#{y7Xr7Xnu${KF;tzB^>3%@{5Q_D_LCS{M&Irm>r1qs@ zyGM}nW0m?7WU4UEH*ZjMa#y2wT>S2MwSRltBfo0;|I6um-FJYeREL$5%(zgxXLS>O(_l4CPP>O<*->3Tr|$Xbu;l zU+DPLk!Qe}a2E83v*8>V0O!I$I1kQ;3t$jj441&Aa2Z?P)P6dzFf6D3p!1}D3CD6eZ|WyH|1DTf{YLGf{)@nJY9I9{ zwR0PmQ~y%Csh@XXIrW2Tzf*tEI9QMMX&fv>j)K&W8R`kD4m{YuB7ex&13ztM52pXm71FT1e+y(-72_EEd&_;gi&*nIqjE@Pv=AHAHjM@A+?Xzi$(n;q~pdRAA={L>i*(Y z);o=S2A)!p)=xm)68E2`kg}CZ{XEuhj(RiL0#dytvbD-X(!KE2P{_rS5>-9a45wsiS)(XYmJGPVKvZ zI-Lhy4<2>O0&*g}2rsGR%g9NP@)hJ1NXNaZQon|L9a6r5dA2}_zco=Iw>pe%#g)iV6 zNXzMbUZMUHW$5pU#KYdyVy}PMMBJ=b4A~3ShQM zE=2wSRiC%$xvvN21wDT`A=C4qGcrAw^+cxUL>FXwUUNmJ=SMeWdX95Prsv9D$n<>Y zflSYvy^-m;&l8!RL;E1p^Pm?pJ)ibPrsu?d$n@OWADNyXy^-m8b^tOxR}Ms`=iEWa z^t?G3nVx@#Ak%ZG4>CO$4@IWu(_zT;yzGly2M&kkkgn?p(&{oJ9?8TwpN{k)+2Cw(5M-skOb9J-II-k(*^e-75K2Lm9rYc6sF7znAo^N<_C z`H=cy0W!6ljzjyS?dWrtK5yyx)KB#JJpucp&*w?Vs-M5~c}t(Gs-K(mImxiSJ?sYQ z^Nr4v`i-tDUAO$MQ)LH+a3wuAb8G4@OSL(ePJpVW`)SdZ>kbe{@Eo$gZ`km)|P z5t;5&VaRT96Qui8I5OR*HY3x0Y6~*mr?w)~eJTQ(?o*M-bf4OW+!t<#bf4OR+#l|Q zbf1bs9sqa2fp9mZ`_vxf!Ei4e0;3__r}iNag)wj#+z)->0XQ5Ugd^ZoI2O|NjzwMy zM?pWh1TKd(uZ|-xfAseNya4uqE8rP89#TI$A*)^|TD}y^>HbF7Wi{$_ zKNyEh*J(0xJiG?aLh9!=$mif%xDE!x^)Lh;f|nun-vZ>LkggM5H@c2=UFka0e(5-L zTsjXrubbHK2DlNX!Z7#{Zh~nr96o}Z;bTbml_$ts;Zqm^pTS6&4(Yy8|1w(3r53ixDURCF)#=2hwtD4m<#E?Lf8L2>NF1M zx?e~A7Nq;d17sS9ACY5W9=rhyAdTZs$Vu=5ya?$&^94B$euk&uZFmRXh4&!c7mAP* zU@@foK{B#bTJeVzWHn@(_a&&`hhO0dco<5s-3OKB)Q>bz^RfIAq;~MgG~cP6=TXk!A3>(`DMY>sHL%`EWa@7j>H_MtU)oL`%XJ~mYnsmr z)M-8|k!e0xL8kd^f=u(dDl*OIYREL7t0U8VHbs7p^R0pW0=XtK&F5OkG@s3oX+GCR zruke4ndY-OGR^0@$TXiVkZC^GL#Fv$ADQNJ17w=d4UuUc)BJ0Mx;E5-G+&z_(|p!P zruk}#O!KuV@^|zTjXSz;x5RRqx6P4h-Zn#~acYH3^MvLDjZYdkG~Q`E%dwp%r14AV z)d6)w*aFhLZ;edjndUjo?{-*D^VkNN?hkE|>AunmSr6Jm>UTO%W7KIL(7dF6r|U!4 zqZ8Ke0vWW1ogvLr+J9rzsavQs^+Kk3<$+A|sy8ytD^FyaSACFaUU?zYyy}Zg^Qs>*&8z;% zG@sfd)BLeRrui`lnZ_Mm$04ZGycmp3^MdBX0MzL^w?X!WG%p7t)A*qK=y23&oDV~$ z@j~N!6zVj-#~{=Artw1Kj>b9Vp;({B`&i_z&<8p|Z%F+{{YdlK9?QqWNpLcx{_KcM z{YLYBH0sn&8pkw#Y24CyrEyB*liE+?k;Wm7KN@#*9cY}<_@eWq@kHZ@#t*fR`kmTI z*Nggz#s!TdI^U^iANAuDWNH_+pT-6C-$<-S^PJWjfjXVv1Y|ldT29BI>q-4X?WBI7 z@vV-}2TiCAb)h~qgvL+~mCyuMgQl=1G=p`Z8k~l9Xdr7r9jFHlpb?Zo1*`(A!s@UF ztOaYsg(}Bgg#54WhksRjseMb(&Y6(%Qsh~1nM(FYUJhrgLMRmrQ7 z1K}E#JP&y-oUfACAuoWzDme(5uKNt+5I7xDKP^V4@k!gQN1gg(12VOr+DYx9^QQBo z^Vo&$c0#9xSK++l%~X`#*&B4nxXEkPkvS{&8eF9vvqZ^%IcVN%dnY%c*^|zoS@w64LQc zBhz{$)z4sgg39)Y5=-2V&Z5v1`5ZD2o1tz6n?qXO0=Xp=u$?t>D|i9**2ou;+aR}v zmr!qqd>OetatD}%dPn3=Fd20l

CERphpA#=zKUPs*y*&g0Ry({u9J^$+z2oj>(Q2G*l~p!PpUo!Up^klJ|<%b!7N7q#;N>M!6UNbR8W$wZy{ zL3MrVN9vC(tWWJG={%_2Z?Rq~q~qiuQ@crOFC9M@>%D>0o;+k~&uipYkj|qJna-Ea z=L70=9(0^{sMC3VM1BuHK{`$WG94!ynWXck?WiAVzxgWLH^F%C0i9HIMkaeAlP<`l zD>CVZOu8eJy^u){WU@Ch>4{ACK_4nQUcB9nuV$-&6v5MUmJzxqv04h7LNNJ>!Lm$PJk2PBsdvPfm5L$oCc@E z8E__?1^wY{I0pu(I2V}=L?-7Ulk<_u1;}I&GD*i>i25S97%owfmM=wJ)nzK{FGpSh zSHe|rHCzMN!gVkhu7@Fzt}9({Y7ZU%f9tq(JQ`2`>~&Wir!<~ue9`!$aYyY8ML%p% zaU(JrhD>fkCc}}*&B){yWO6Gq8G%ejB9q&Y$?eGG4rFpCG8u(T?m{MaBa?fO$-T&A zG%~plnT$au_al=BkjaC{;BgE-`4@HmWD@dWZocuK|7$YcUC zc?Ow0i+m2AhddNuBD?@Es(1-mRl2`k#&R+V`HG6k$YcsKc@>$Yeju-5IZ6FY=S}O; z^`d@Ri0!VccmtWFcGLDZv7DsqN13*}h4oaujrtvU7v6*SA&nzCKWZ2CLx9Tqv_0*Q z_D{#7{-EnBV0*gmG!7o1o(dnTn1)O~LM9(0lTVPzr^w_pWHKF@%s?ieBa<(X$(P9F zD`YYgnS6~*zCk9lkjZRh@+~r%gG|0dCUcR=_sC=(vZ^0Y&sXsy@+TDwkjX-1@-s5| z1(~Gdd_|osLMDrm$r9vJSO&kr@334&i4+AWg=#9QBa<4)q$V;+=dFc0N$t}{T}MSa zJ}uY9a#9ajUqx!a0qQi5X#c9)(f$mvK3!)cWa=+tWL0IT(|#$_{^&etKh!VOf3#eV z{V1SP#VW|82{Ks~nXHCPR!1gHk;xj!WKCqU7BXpuOx8vw>mZZn$Yfn)(gK;RhfLN- zCL17=4Ux%4$Yf(=Rq1*(!E#F#n<87mW-2yECR-r4gw`syLMB@ylWma6w#cf|{@Y=B zdlfq%cl;eYp-$Q$leWlooX*H(7i5w{CK)nmhfLZdtJ)QHlG;W6LEF=EYCrW4%^y1d zZrGoy4ydcz9d$<)dmxie$fPqe*%O&`K_*?1NjGHD9hvNfOnM-by^%>zWU>!3>4i-8 zMJD?pll_rNZ)B4CcL3@G;UG9z#UaQ(Dh@>^havl_I2?I|iX)N9QOKiJ9D_`bMINW( zcw}+{vZ@nNS9KEVp}ac>rCfO>V1(fFj}(Y&JlQk}*NjYqnUG@fZW)oDH2&omr=I-CJ#!dcKC&W3Yf0Gta0;XF7W zE`ULBp^A%;7sDlRDO?7Z!xeBPTm@IdHE=Cl2ZLc9JYG_}sUQAT$EWuHtNMFA+8qKz zf5#1|Z~Ps@P~Y@BhNHgucie*d*55G#^~m3G8|vGC#~rBe{2ilE-=*SiWL5W|t}68d z^&_>L+DGl6^QH5mxXgpN>nJ&WGAd z$D#95JR#OP2rZi3-(Gu#5V!Uz}% zx54dj2iysx;4Ziu?tyz@G~5Sc;C^@j9)ySBVHgXKz@sn@9)ri>|7*uCgS@Ju0D%98 zUAnuwOGOa`0TFhAC6?F)K|n!FB&DQ71qCIPR!~X_MFr{Z?(XjH2A7>5=*)icn_sx| zW_~m8o;l~vxijzHc{4bZvpAb`IG6J{p9{EtLmw1_1 zc$L?9oi})sw|JX(c$fEhpAYzuj~IyM{AVl%F_^I#hjAH?@tJ@LnTUy*gh`o<$(e#F znTn~IhH06O>6w8UnTeU1g;|-6*_nemnTxrZhk2Qg`B{JkS%`)C9G~Y4EW#K05{vR> z7GrU~!V)aWS6PasS%$B%EMI3imS+eiIUoA6z}$M^XGKjcSj%8%KM&Dnx2*@~_C3EQwO+p#@6@KbhV zCw68Rc4aqqXAkydFMh_)`2~Bk55MGB?8|=qn%}TL2XG(<@mqe!!5qS&9LC`s!IAu) zKX4RB^GE)~F&xWr9M1{-nZIx%Cvh^T@K^rEsr;RP@J~+TbpFK|oXJ_7%{iRQd7RG$ zT*yUS%q3jPWn9h`T**~j%{5%hbzIL4+{jJb%q`r?ZQRZs+{sl z%p*L?V?53iJjqi$%`-g9b3D%ryvR$u%qzUgYrM`IyvbX<%{#oyd%VvFe8@)(1Udg1 zi$M%#Y{p?+#$$XYU_vHhVkTiyCS!7@U`nQ9YNlaYrek_$U`A$QW@celW@C2dU{2;@ zZsuWL=3{;qU_lmQVLr#_`2vgZMZUzMe3`{qoUgD1OY&8gVriD)Yb?vxS&roy!cbOV zMOI=ME3*o#vKqr#oi$jKwHU!j)@B{P!Md!+H(8$z*pP4WZ8lQ9odPU*@a!%josOUJ=u$&@pFE`-t5CK`4#)J zAHU`|?9Txl$U*#;-*GU9a43gyI7e_KzvmAe#nJqcKXDAlavaBV0)OT&oXAO>%qjep zzi}#m=O6r&(>R@faRz5{7H4w~=W-tBa{(7}5f^g_mvR}Ga|Ks&6<2c&*K!@#a|1VW z6E|}Uw{jb|a|d^F7k6_H_i`Wi^8gR>5D)VRkMbCg^8`=w6i@RE&+;74^8zpO5-;-# zuksqN^9FD77H{(o@A4k+^8p|75d*=_f5u`EgBhD~7?<%Fp9z?diI|v4n3TzwoGF-+ zshFB+n3n07o*9^tnV6Ybn3dU>ojI73xtN=In3wsOp9NTug;=3dNV&k1k6Lug2en(D zF(|72i5_o3Xhe-bvUq_&)Dtzz1|Hwep6t-mppdX?F@pZN#>o^}FJ`9bq|q6Jqbfv| z3ki(~q>Os;_$N?whQ~FcXKIH>#+->>VnnUT7}fusI6A0S=zpjlUcFk(8mfoIoQX~x eUN>g-N5<0zh1U&>vBW1qPnP=3XC3$-I`B7Y$N6Xg literal 0 HcmV?d00001 diff --git a/resources/ShapeRecognPlane.med b/resources/ShapeRecognPlane.med new file mode 100755 index 0000000000000000000000000000000000000000..c06ecfbf5db039d644afa96d7bc55e0412d98d4c GIT binary patch literal 12832 zcmeHN3sh9c8J=AN3y9Ik?)pH5dx;gR;-Vt4jd5Tf>>|5Mc2}deVQm3vd{QkI>x0C^ zqt-`21dSF`Uo|nQ8ck0180!#{TE(cTC#Ra^n5ZWzHfiihtgb(eiyf*4E7 zoTJ~~xij-rYLLC+@HDA`im@zP49tw{D0bO z#}xzxi7iFIq*R==;L;zqX1UA;fg^#kU~i-_ zr50v%AxLHPPyaZ=$q#)xGK!Bl$jf2}tU*65DJ4sN zI_Bz*EGOrErhG9|znb*(A=|j&>$Bv?K4g<~;u?w8XUd7&gKv>3J{>DBeAHw)Zc$tgi9%;IU?jqPyS(55;=c z#7xAKScq>b1>uSf-}psb+eB9Yzqs)h11+r^m^Y!7E9OmT<%%{#m=%Y55)!rOqVt4- zk0g!tCL|-fkde)TelhA3a{Y+^rtFT^ZAzaV$bw`N9?_EBmWfM)8(EpCf|HUPysHAp z`aBt&-fsUJC@;8yi7Tko9!f6)L2veFt${Z}PUhDP(@XEz_2zNG6A_qivl~-@_LmLA zqJtY4Q78l&N`*o(11KOysEHz&z$2rHLa9_r5{eeG(g=kLxSImUE%;9Tm6H{yoJWBH zzky+uljSFrHa=AKO6$62MRJ33bv#v{)>Xf-R!to>=^tD;qoHR%$=XzKPD@{1zv^py zT|9kv$Bl&A8}an~{(Qr=L2BAmcXUQZzM5X0IQc}iLQVB4mG7s_Q`6d;M^7HQrlz<4 zbmx@Kt)VlwZ*EGB*3#wsRQ$rnwR9^h$vIY{rH4rX_=v*fBktiJrln%ae`Ar zhh93J)Ob`wBZ}+)c`;p0^GE&rM(Hj!JrJ38GdfX2dwf`YYg3gVhts={CtmKw-ud&j zlk@lYWi6k#q;o|Luk>cmY^-XYH@y!_YmVH=rzWtbg0hh(uP1oNV{_9M*Pfyrdv=)HdCkGG!;0gH$KT@F8>wBaW_+tG9B zy5g=pdui)M+o*#a+Z)mN%${vL+rGy1=wdt1HuTnn<86^!B8%7prq;f)+C|6S50Qq8ey)xbOR%Xzk9?F$FYgL$^7-^DpS|H`pBuZ$gf zc?rjs$C%9SnLJy#ZksZ-H_u8E6^C!X%dx^;`M+!^;aD4w=Ksw!3^y3eFmHiEgG7QP z@M8exPgo|#z{Al1ALLGsugY<&_ z-he`dAvLUK`oeQR!2W=VfJuNHAP<-fh#>*xBPRzOCnpb-ubf;^A9C`M;|temFTT(& zwWo>V7zr7`0^qDTC+M#Rg*lxqahtA%CD+%8i&_G2xx^Ng*z4m zJ)%fv(0x*)j!NJMd9kdT0?{Yv7HoEd3!+zWa^eal5Z4ZX z5bhRcDH!Q6!orA2%M)tkEL8mXCpJX+)4LP=JiWKmi_yG=FA7urw(IFVm8Q>YA4i*Q zvR5asrZ1*M$+umF|QAKIy${?N|l_JQ^< zw~q&HhjRQqC^@5Fkvk`7FRj%Jj{k4nH@J;X=#)T*kU*G<%st4ngHYv+6DqdgiKJN0 z%>JHdg}`!V;6RX0i*lyN^r>3e6vpKkQ2}2fHsueRFP9G104YTIR9OenW-mm6;5+oo zfP_ya)cqNDKpad;jB_ypisKnj=% zm<6Z@Gyoa_O^9Yg7QxHBQeS21i3D% WLG$k2HZk;NF=Vw9J0QDa0=UyLob?+*O8(E-FS@q6FPTHdw7Z~ptVz4y6u?>TqI$;3cE|0-rS zW`-svB0ApS$nH{XsgHsRaT~ja2l_=CDsrthb$j=}G($^W9coB4GZYz!L=|mB0;&aR z{llXY12kGqq)(_OYCk%yD6}*XRnlmq62p9aGKBiNqheK^wSvD-)WV9z7E#d%c>NX! z8{M%k-K7m5a!G6{GAik=xsym#K`1cS6^8i)i>eDbDNYL$846^FjN%{Ge8_K77yrn>h@#WZP*nX-_vt+~ z0<#=KC!;>mr1GClKln;$PCW<{q&r*$W+xTB6#i^GxcP){bfBN$kub7wzKOcHEJTaS zA{wP>r`;a@Q9>;d`A2U7BK2n^iV&p+2qN2Jyyck%H`08gL;ZzuE9hzp z7m+t_M52YQl(x0J0QhD)a)HYO?^uez7{X#VM z9m9b7dnSnT-)V9$rPUOX{X4_nnfvIXsQo*`s)Nn%h$QbcIsUTtGf~nzhK%cZ&jWMj zbkLbTAAczWfA@-5zY>Q1G*`sa;dea#ic3=6xo+<76BZO2s!5F3M27Dt$6EZQy!_c{ zp6|lU(p_3_s1mB-DqQkjN0$?`ih^&t^IG%~_e5<>ScyB>TVI;Kef7V8(Q8loH>KZe zMjKQYE`mGFiut%<`~{8 zFr@T_2-<7IH7*#t@R*xP=_i=Tz`$BaqJe?IcEn7qU@bBzE#bx{A_GH1!=lUEG(wtpda9zR*9Q^K8Tqa{NO>1t9dLv`btutS5c_?LO4J+L*xGrVu(={Pu zD#)3I(sH&WOU7(7lha4tlQ19Cu(^wtiCL=qK+_h#NZB{92JipQNY0*D(OMjsD`V5L zUIleFkg~!Mk*`s%nB6?GF|i=lnyD7g@)Do6VyhbN-!b^4n2mlJQsveVYc@0Rbh~*1 zYbtgPv(C3-QT_Ulx1DUw*6g2q;<7N#B$ucE9=J%x0)1?k&oY&<-%U<54D^(+T|FM( zzv(7rbvE_aK6DYY87-POA7mk6+2fCFn=(|!GRFP<^`ZnB>*nflJomYjg;bs4@Gwcj z_SX`dj;k(Z8`q>xeVHX?U7~N@I5t_vE>uabDw}M@yxkwqNo!%n{`E}tTBEgMcikO4 zCmyzD`QBf@QQZ_X=a$CjXJ=Wn8s-}|diIsErOyuSd+jS_&YI-BUcXA&vNIuFCoh#T zogH6F)dq*z2~GEm}9*E#;&&JGWt&XsnBr zEm?iPYj%!=oqaR^k9h|rtXi$DKf1RSv-^AVUaadYWd&ny{*Wod|2yyQug$= zGA!R(!g^dZ$*j*TnY(=0ocTj7S^553Hcu1((>jJE0*kjE4|fxD>ieG$6oh53l{dGOXo?q#cX!v?(T*@*378UV~4u- zQkK-E<^Z`6-{^~h1NXa27_S<6X2mTFcFi*MaN-I}*3NOjxmg=5Sz?z{`C^fjWt;#fdNqi-zOQP&28(uFu`HjVt@>SPPH$ic&FmXRe>t0q)!bHrA4LUTbC7d#e?53dq02v=%I7&cnr3Cs?z)#?JA5 z+$1bDK4;tRr(&kbm@wM4f)%^HweW;{6*0R})olJmk6;rlv zerjj51kb?K|INWR*A=U#%ekY%Xz1w8a(;hR?FqRX zmip84Z297!+@soGvE?lT9{n_WlPy=~WILT+Am>j~ z8=S1RN2ou|$msPBIbUsG@sZ&wIlp{uey5tRZFyYsnH8%p+VT|X`Q8VGer&~)X9ft` z)#a7j&&K^-aBmgglj(=W&GQgr{(Tw-g3R1yQ!D0 zws>mGBOmRJuX@^sFXt0BMi$6$q0N zN2R5o{N;N)9=<(bZN0~KJlg$PXnSjWp1Ai}K=36yzOk8Oc*r%OeErYUYAliQfS9{G zO`qHG_&ZA!d0)zS{P%HQX{YRX)qoex?h5_3oN<2!|5_Ng`oOH%%eLI!)9B8UHMacF z`WM0flGtWpDCG~@&D=R-u|0n*Ia0fCZy8_mo3_%ES@!%zirJKB*Q7k^$nLuh^Q2r>n0kJ| zRw*CV?uh2%xAr{P)2)5CBldhrsOI3w-|cyi-zT525c(U^Va$vaZqHBpN$TI+Y|l-y zzw6p;iImsdd#P#sQd^#6vBcHjl`TKr)8Wp_7&*6#{ywhWYB~Sm;^_6ua_srb+Dj$l zc1ZcjjkR(Io|EyMtxYdEnaX(cur+DzuGn$orS6?~p0ekU`kf51&a>z7?JQPYSuEo% zOm{f743+X)?7$@_@-w@q+VHd%buSkL%lP=OHi!mz34WPM4o_<3z@1;z zziSjAJ?l z?Ms`cM_$t&~6NqP7e51KvOs8y&dmtKL4-JKONQLzjIrlu~Y@ zoIEREYR7jci1RM5mvZl};`AF8r2OtwXHDc4DW4v`{O*}N3AeesbdvnEjQ`s3PV1l7 zI`FW`lXqL!l=7#|POrb>Y0KBS=c(tMlW?;O%lfu3Q-C$}G}#qA-tJ=SdjD4QnAn7Z zfveU0+d6lbxHTrNLOT`bZ3>}Um7 zq=uNScUJP#n{Kq6&{oBN3rXu)yStiSomb=h*@J3++iUIC;{__-#D3-v+cqot?BB12 zbXuk0b+S^%*4?P!H}Z09O!p{xWH;{?<&k*wyf zzPII?)++vV^Q;cuj!JH|edJ!ZHVW?l^Pc(Z@2h#MJ~6MPcho{2*L^dsK+PTZr%S(g zQt-MJ&TU(NPt8?JVyq%+DR@9;oa2|y3O;|-g`~&!3hpsv+qCOS1@E(CL(ESmD(;$8 z?M+QQ}zo)&gd z^7HjB9vUE13gfL&Z5gcK(okfY`cUcAn3GF8pj&()|F z4^VLL^W7yo=c&1Q8aE#8qvo53x7qY0SH+Wu51nnlTEWNf7~phir;?9e9`Vca<|=-t zrj_C8?-YERcJ9&9UP>8UN>;ez<}jS?z#7IN@pQ|2m7@d^;LZ}|Do@}g?R@Q zoaLT)xVw#-YxgESPDoMl-DkdE&V=(~*&UCDsU}K3^vs-qZYp6OS2{GjxKqV*Z+oY@ z#He{r?CDnv&nx-C-uHhRQA^3^3|V)te!P-j?Q+qyWubyUIUaa*p_A}_7$3CYx`CQ6 z8L|7?)gdbG5_eQ2zogTkozA{(3 zsUKJItbo@!5ot;;@rp#5@MmFlzWvXy+s#0Q7B2=lXMd+b`DeMa`@YJEcA|D@tWdrrO0X;g_v4D{q6IJ;Uxd0TGU@e(VT ziwxfV(52*7`;<}$LOAB)5%hm-s~ucC?x*=%Ve1I9PoJCaHH*FbXXnOoK}1aXk9K0h zhT;{2$_l;|D69q03rF;C|B}QAygEIOk_`QlBoSjel?rqYu^7Bt~p$*W4 zm$-g*LsmaJIK7HxUy-A$H+C#5hO)?4-ku1cjROZS$Qx)t|O@-2G`CHimK*nvjE=b@jE&l&iffzKKE@5%se z3h2X1`dk{Tc#$#Or7wHY=i~G>AX;7F=?>wezoJ777JZ{bcrAQnUv%iUO}YagQ0l%xQgqO_Rj_~)VG$|9N=!tnDh29e z1t(HLt3g^i3Ey2QIlFKz}d*3m%25bw)f$?BFusxUn(zksQp_9OlU?;FM*ahqg(&Dc> zbPup6*b7VsdxI%pA21c{3-$y1g9E^U;1}Q^a4?t#4grUP!@%L-mtZi zfn&h2;5cwRI02jpP69K)$>0=lDmV?C4rYQgz?q-{SOGKwjX^`OB3KDD0Zl4 zRspMm)L%8|>YxQ!mLKHjr}BgPE6We^6K?(2;L~qkvT+HdGo|zu`Fne;|H3V4!)hBu zGp3+}YwM4kfYlO9{@-_mcjf+umnB z7#jC~x4l1x_tHlPr&qD;?S0nuw4CzZACy{R`Ewc`UhMFHVD=NO zAaR<1#^(%t!VJ*5h1Mr!t(QL4I;pJnktJfHbsequXdOuFL-K<(l~a9MAJV#!)|0f( zq&P`aIn}53`m}<7)}R=afKrh5x42=y{nr=z`&x>ly!|ctOH^ST%99$J%4xPWh(!iG-b=t5E!Yu+z92Xn&C64uhuiFaR10ZxPu^lRxx)g8Za@LeRb) zNPfgalfUFod)TSJ4$#zJN9YKUbQ@^$Hx8PfuTY#=Y>Ftqtx>N#Nb?s3-5I2OQ#@T@ zr?_Z-DK6@-6Y7zj{7i(MG{w^!_HJMdmfHOeq zCli|dB|R1PX&}Wn40;Mk{gc1xuv0%Y&y!#$KdV4j22H_AdL%p5qju(~X9k*p74=AV zsz>c?P_HVe0%;r?w>*ESKkA>xBR>p3(jN!Zs{xW<2GG=x6ExZNN$sprPYkL-N6-b7 zfNr1yRDxDuO|U*l`SXOP{L%H^5_VgV`l$o$3X=b|pecUJM+4ZYzedotLAr0+LED2Z zK`H1B(mJy-H09F=n&PJXc)?EjbB3n8lE2Mi_XWuximNH?h$_MpN_Zf|H>Lt1EOY*bN*G zQa<`a_XcSm=>C%oJNZxk(RoCEMWbE{*axKcl;1C4C;!N=!LZYO)A>mL_e446o8qNB z4@J5D_(`ynKNLTWKMds*Z+GYsU@A!VG-%3K4`^B+jE0^BjshtUgP;e18DKwfGD!29 z4m}2>e#pNsVW;*~eM6gmKRSA)OP}F>XEL)L>H9T0$st{U>&e7SPygq={ns2n&=Mg z0eXTB_1FlS*ch5fel&qypG{$>dS1}Yz~*|S`y#O=%8B0445a>jpeY}|&~zTOf~LHX z|9-F&HPA!`?XO45ALTs&AZ7zdL7WG6r3QQi)u>nt$=cJi|WG%*pHNcEFoC(?M-j{NF~diwbz zdnc3=J41H?yXukdpWR{aQ3iX$PV5DptjFHa#1!a0dZd16{8W_dlkSW9q<;IM9r;7L zKkRfK4S*&Ng#JR0bUqD&eK43-2Ft291oeq@UmONIaX9psWiTD~5#UHYj)EQyepLp^ zk1;483yuS+pYhQ8oB;boko=kiP4Q6w)SlX9pdOuHlc6at>X-6B?M9;B6mV)8B!4I# z8gClv6Q@J#GZS{oBaKI-yi&axsIO1T1Lc?6&qRHFQr^iw>W{{u{L=W;p7KTWO5;&n z816GZXW(-NK4;)_20mxta|S+V;J-ElR`ARk6oV2_3d%q^_;;VHsL+oZtN}WKH9=?4 z1*`?Sg0;aqU|o>*&u-B5!3Ll^=mC0y4Z%hr?em*JHwDXj4$%VTEkSRPfj*!w*b4Lm zHK0Ej00x3VU@#a0hJs;WdHelH)QbXX|3&-vSlG+jrc`ZpY|QJ z-=X~l?cZoWLHjBF{T%H-Xn#WcE!x-7zK8Z}w7;SK3GHuazcUkl%>rkGbHJ~`x!^o- zKKKo|09*(z0vCfzz$|bnxC~qlt^ikptH9OZ8gMPR4qOjz05^i!;J4tvz)j$H;AU_O zxE0(6=78J59pFwd7u*Hz2KRs*+zajl_k#z(gWw_XFn9z!3VshB1CN6zz?0x9@CWcT zcm_NR{s^7}&x04hi{K?N54;Ti1YQC2!K>gk@H+T2cmuo%-U5FCZ-c*rcfbPhE_e?t z1n+|nz=z6p-~MR7CMu%D?h%v^(#fkmFx+1ih3&gaUDNRRMC&?=$tF+LFY(ONsKnSWJ%HgS0E~~IR-`#Dp^w0@rQm9`E3@pZOi`cVsiB}KWE_oAp`#dev(1J literal 0 HcmV?d00001 diff --git a/resources/ShapeRecognTorus.med b/resources/ShapeRecognTorus.med new file mode 100755 index 0000000000000000000000000000000000000000..7623db578bc12a32a74e0d6041f51ee64ad00ead GIT binary patch literal 43247 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 literal 0 HcmV?d00001 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 14229c770..e4d70409c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,6 +82,10 @@ IF(MEDCOUPLING_USE_MPI) ENDIF(MEDCOUPLING_BUILD_TESTS) ENDIF(MEDCOUPLING_USE_MPI) +IF(MEDCOUPLING_ENABLE_SHAPERECOGN) + ADD_SUBDIRECTORY(ShapeRecogn) +ENDIF(MEDCOUPLING_ENABLE_SHAPERECOGN) + # Application tests configure_file(CTestTestfileInstall.cmake.in "CTestTestfileST.cmake" @ONLY) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/CTestTestfileST.cmake DESTINATION ${MEDCOUPLING_INSTALL_TESTS} RENAME CTestTestfile.cmake) diff --git a/src/ShapeRecogn/Areas.cxx b/src/ShapeRecogn/Areas.cxx new file mode 100644 index 000000000..f8441cb48 --- /dev/null +++ b/src/ShapeRecogn/Areas.cxx @@ -0,0 +1,485 @@ +#include "Areas.hxx" +#include "MathOps.hxx" + +#include +#include + +using namespace MEDCoupling; + +Areas::Areas(const Nodes *nodes) : nodes(nodes), areaIdByNodes(nodes->getNbNodes(), -1) +{ +} + +mcIdType Areas::addArea(PrimitiveType primitive) +{ + Area area; + area.primitive = primitive; + areas.push_back(area); + return areas.size() - 1; +} + +void Areas::cleanArea(mcIdType areaId) +{ + cleanArea(areaId, -1); +} + +void Areas::cancelArea(mcIdType areaId, PrimitiveType primitive) +{ + cleanArea(areaId, -2); + Area &area = areas[areaId]; + area.primitive = primitive; +} + +void Areas::removeArea(mcIdType areaId) +{ + for (size_t nodeId = 0; nodeId < areaIdByNodes.size(); ++nodeId) + { + if (areaIdByNodes[nodeId] == areaId) + areaIdByNodes[nodeId] = -1; + else if (areaIdByNodes[nodeId] > areaId) + areaIdByNodes[nodeId] -= 1; + } + areas.erase(areas.begin() + areaId); +} + +void Areas::addNode(mcIdType areaId, mcIdType nodeId) +{ + removeNode(nodeId); + areaIdByNodes[nodeId] = areaId; + Area &area = areas[areaId]; + area.nodeIds.push_back(nodeId); + size_t nbNodes = area.nodeIds.size(); + area.k1 = ((double)(nbNodes - 1) * area.k1 + nodes->getK1(nodeId)) / (double)nbNodes; + area.k2 = ((double)(nbNodes - 1) * area.k2 + nodes->getK2(nodeId)) / (double)nbNodes; + area.adimK1 = ((double)(nbNodes - 1) * area.adimK1 + nodes->getAdimK1(nodeId)) / (double)nbNodes; + area.adimK2 = ((double)(nbNodes - 1) * area.adimK2 + nodes->getAdimK2(nodeId)) / (double)nbNodes; + area.adimKdiff0 = ((double)(nbNodes - 1) * area.adimKdiff0 + nodes->getAdimKdiff0(nodeId)) / (double)nbNodes; +} + +void Areas::cleanInvalidNodeAreas() +{ + for (mcIdType nodeId = 0; nodeId < (mcIdType)areaIdByNodes.size(); ++nodeId) + { + if (areaIdByNodes[nodeId] <= -2) + areaIdByNodes[nodeId] = -1; + } +} + +mcIdType Areas::getAreaId(mcIdType nodeId) const +{ + return areaIdByNodes[nodeId]; +} + +bool Areas::isEmpty(mcIdType areaId) const +{ + return areas[areaId].nodeIds.empty(); +} + +size_t Areas::getNumberOfAreas() const +{ + return areas.size(); +} + +size_t Areas::getNumberOfNodes(mcIdType areaId) const +{ + return areas[areaId].nodeIds.size(); +} + +bool Areas::isNodeCompatible(mcIdType areaId, mcIdType nodeId) const +{ + PrimitiveType areaType = getPrimitiveType(areaId); + PrimitiveType nodeType = nodes->getPrimitiveType(nodeId); + return ( + (areaType != PrimitiveType::Cylinder || + nodeType != PrimitiveType::Plane) && + (areaType != PrimitiveType::Sphere || + nodeType != PrimitiveType::Plane)); +} + +PrimitiveType Areas::getPrimitiveType(mcIdType areaId) const +{ + const Area &area = areas[areaId]; + if (area.primitive != PrimitiveType::Unknown) + return area.primitive; + else if (area.nodeIds.empty()) + return PrimitiveType::Unknown; + else + return nodes->getPrimitiveType(area.nodeIds[0]); +} + +std::string Areas::getPrimitiveTypeName(mcIdType areaId) const +{ + return convertPrimitiveToString(getPrimitiveType(areaId)); +} + +int Areas::getPrimitiveTypeInt(mcIdType areaId) const +{ + return convertPrimitiveToInt(getPrimitiveType(areaId)); +} + +const std::vector &Areas::getNodeIds(mcIdType areaId) const +{ + return areas[areaId].nodeIds; +} + +double Areas::getAdimK1(mcIdType areaId) const +{ + + return areas[areaId].adimK1; +} + +double Areas::getAdimK2(mcIdType areaId) const +{ + return areas[areaId].adimK2; +} + +double Areas::getAdimKdiff0(mcIdType areaId) const +{ + return areas[areaId].adimKdiff0; +} + +void Areas::computeProperties(mcIdType areaId) +{ + switch (getPrimitiveType(areaId)) + { + case PrimitiveType::Plane: + computePlaneProperties(areaId); + break; + case PrimitiveType::Sphere: + computeSphereProperties(areaId); + break; + case PrimitiveType::Cylinder: + computeCylinderProperties(areaId); + break; + case PrimitiveType::Cone: + computeConeProperties(areaId); + break; + case PrimitiveType::Torus: + computeTorusProperties(areaId); + break; + case PrimitiveType::Unknown: + default: + break; + } +} + +double Areas::getMinorRadius(mcIdType areaId) const +{ + const Area &area = areas[areaId]; + return area.minorRadius; +} + +double Areas::getRadius(mcIdType areaId) const +{ + const Area &area = areas[areaId]; + return area.radius; +} + +double Areas::getAngle(mcIdType areaId) const +{ + return areas[areaId].angle; +} + +const std::array &Areas::getNormal(mcIdType areaId) const +{ + return areas[areaId].normal; +} + +const std::array &Areas::getCenter(mcIdType areaId) const +{ + const Area &area = areas[areaId]; + return area.center; +} + +const std::array &Areas::getAxis(mcIdType areaId) const +{ + return areas[areaId].axis; +} + +const std::array &Areas::getAxisPoint(mcIdType areaId) const +{ + return areas[areaId].axisPoint; +} + +const std::array &Areas::getApex(mcIdType areaId) const +{ + return areas[areaId].apex; +} + +std::array Areas::getAffinePoint(mcIdType areaId) const +{ + const Area &area = areas[areaId]; + if (area.nodeIds.empty()) + return {0.0, 0.0, 0.0}; + else + return nodes->getCoordinates(area.nodeIds[0]); +} + +void Areas::cleanArea(mcIdType areaId, mcIdType newAreaId = -1) +{ + Area &area = areas[areaId]; + for (mcIdType nodeId : area.nodeIds) + areaIdByNodes[nodeId] = newAreaId; + area.primitive = PrimitiveType::Unknown; + area.k1 = 0.0; + area.k2 = 0.0; + area.adimK1 = 0.0; + area.adimK2 = 0.0; + area.adimKdiff0 = 0.0; + area.minorRadius = 0.0; + area.radius = 0.0; + area.angle = 0.0; + area.normal = {0.0, 0.0, 0.0}; + area.center = {0.0, 0.0, 0.0}; + area.axis = {0.0, 0.0, 0.0}; + area.axisPoint = {0.0, 0.0, 0.0}; + area.apex = {0.0, 0.0, 0.0}; + area.nodeIds.clear(); +} + +void Areas::removeNode(mcIdType nodeId) +{ + mcIdType areaId = areaIdByNodes[nodeId]; + if (areaId > -1) + { + Area &area = areas[areaId]; + area.nodeIds.erase( + std::remove( + area.nodeIds.begin(), + area.nodeIds.end(), + nodeId), + area.nodeIds.end()); + areaIdByNodes[nodeId] = -1; + // TODO: Update the parameters of the area ? + } +} + +void Areas::computePlaneProperties(mcIdType areaId) +{ + Area &area = areas[areaId]; + const std::vector &normals = nodes->getNormals(); + mcIdType nbNodes = area.nodeIds.size(); + area.normal = {0.0, 0.0, 0.0}; + for (mcIdType nodeId : area.nodeIds) + { + for (size_t i = 0; i < 3; ++i) + area.normal[i] += normals[3 * nodeId + i]; + } + for (size_t i = 0; i < 3; ++i) + area.normal[i] /= (double)nbNodes; +} + +void Areas::computeSphereProperties(mcIdType areaId) +{ + Area &area = areas[areaId]; + const std::vector &normals = nodes->getNormals(); + area.radius = (2 / (area.k1 + area.k2)); + std::array center{0.0, 0.0, 0.0}; + if (!area.nodeIds.empty()) + { + size_t nbNodes = area.nodeIds.size(); + for (mcIdType nodeId : area.nodeIds) + { + std::array nodeCoords = nodes->getCoordinates(nodeId); + for (size_t i = 0; i < 3; ++i) + center[i] += nodeCoords[i] - area.radius * normals[3 * nodeId + i]; + } + for (size_t i = 0; i < 3; ++i) + center[i] /= (double)nbNodes; + } + area.center = center; + area.radius = fabs(area.radius); +} + +void Areas::computeCylinderProperties(mcIdType areaId) +{ + Area &area = areas[areaId]; + size_t nbNodes = area.nodeIds.size(); + // Project the nodes to the central axis of the cylinder + std::vector projectedNodes(3 * nbNodes, 0.0); + area.radius = 0; + area.axisPoint.fill(0.0); + for (size_t i = 0; i < nbNodes; ++i) + { + mcIdType nodeId = area.nodeIds[i]; + std::array nodeCoords = nodes->getCoordinates(nodeId); + double approxRadius = 1.0 / nodes->getKdiff0(nodeId); + for (size_t j = 0; j < 3; ++j) + { + projectedNodes[3 * i + j] = + nodeCoords[j] - approxRadius * nodes->getNormals()[3 * nodeId + j]; + area.axisPoint[j] += projectedNodes[3 * i + j]; + } + area.radius += approxRadius; + } + // Axis point is the mean of the projected nodes + for (size_t i = 0; i < 3; ++i) + area.axisPoint[i] /= (double)nbNodes; + // Radius of the cylinder is the mean of the approximate radius of each node + area.radius = fabs(area.radius / (double)nbNodes); + // Compute the axis of the cylinder + area.axis = MathOps::computePCAFirstAxis(projectedNodes); +} + +void Areas::computeConeProperties(mcIdType areaId) +{ + Area &area = areas[areaId]; + size_t nbNodes = area.nodeIds.size(); + // Project the nodes to the central axis of the cone + std::vector projectedNodes(3 * nbNodes, 0.0); + std::vector radiusNodes(nbNodes, 0.0); + area.axisPoint.fill(0.0); + for (size_t i = 0; i < nbNodes; ++i) + { + mcIdType nodeId = area.nodeIds[i]; + std::array nodeCoords = nodes->getCoordinates(nodeId); + radiusNodes[i] = 1.0 / nodes->getKdiff0(nodeId); + for (size_t j = 0; j < 3; ++j) + { + projectedNodes[3 * i + j] = + nodeCoords[j] - radiusNodes[i] * nodes->getNormals()[3 * nodeId + j]; + area.axisPoint[j] += projectedNodes[3 * i + j]; + } + } + // Axis point is the mean of the projected nodes + for (size_t i = 0; i < 3; ++i) + area.axisPoint[i] /= (double)nbNodes; + // Compute the axis of the cone + area.axis = MathOps::computePCAFirstAxis(projectedNodes); + double normAxis = MathOps::computeNorm(area.axis); + for (size_t i = 0; i < 3; ++i) + area.axis[i] /= normAxis; + // Compute the angle of the cone + const std::vector &weakDirections = nodes->getWeakDirections(); + std::vector weakDirectionNodes(3 * nbNodes, 0.0); + for (size_t i = 0; i < nbNodes; ++i) + { + mcIdType nodeId = area.nodeIds[i]; + for (size_t j = 0; j < 3; ++j) + weakDirectionNodes[3 * i + j] = weakDirections[3 * nodeId + j]; + } + std::vector angles = MathOps::computeAngles( + weakDirectionNodes, area.axis); + // Correct the angles > pi/2 with the supplementary angle + for (size_t i = 0; i < angles.size(); ++i) + { + if (angles[i] > M_PI_2) + angles[i] = M_PI - angles[i]; + } + area.angle = MathOps::mean(angles); + // Compute the radius of the cone which is the mean of the approximate radius of each node + area.radius = MathOps::mean(radiusNodes); + // Select extrem nodes + double q1 = MathOps::computeQuantile(radiusNodes, 0.1); + double q2 = MathOps::computeQuantile(radiusNodes, 0.9); + std::vector q1_indices; + for (mcIdType idx = 0; idx < (mcIdType)radiusNodes.size(); ++idx) + { + if (radiusNodes[idx] < q1) + q1_indices.push_back(idx); + } + std::vector q2_indices; + for (mcIdType idx = 0; idx < (mcIdType)radiusNodes.size(); ++idx) + { + if (radiusNodes[idx] > q2) + q2_indices.push_back(idx); + } + std::random_device rd; + std::mt19937 g(rd()); + std::shuffle(q2_indices.begin(), q2_indices.end(), g); + // Compute the height of the cone + // std::vector heights(q1_indices.size(), 0.0); + // std::vector distancesToApex(q1_indices.size(), 0.0); + std::array p{0.0, 0.0, 0.0}; + size_t min_q_size = std::min(q1_indices.size(), q2_indices.size()); + for (size_t i = 0; i < min_q_size; ++i) + { + for (size_t j = 0; j < 3; ++j) + p[j] = (projectedNodes[3 * q1_indices[i] + j] - projectedNodes[3 * q2_indices[i] + j]); + double height = MathOps::computeNorm(p); + double distanceToApex = height * radiusNodes[q1_indices[i]] / (radiusNodes[q2_indices[i]] - radiusNodes[q1_indices[i]]); + double orientationAxis = MathOps::dot(p, area.axis); + for (size_t j = 0; j < 3; ++j) + { + area.apex[j] += projectedNodes[3 * q1_indices[i] + j]; + if (orientationAxis >= 0) + area.apex[j] += distanceToApex * area.axis[j]; + else + area.apex[j] -= distanceToApex * area.axis[j]; + } + } + for (size_t j = 0; j < 3; ++j) + area.apex[j] /= (double)min_q_size; +} + +void Areas::computeTorusProperties(mcIdType areaId) +{ + Area &area = areas[areaId]; + size_t n = area.nodeIds.size(); + if (n == 0) + return; + std::vector areaNodesK1(n, 0.0); + std::vector areaNodesK2(n, 0.0); + for (size_t i = 0; i < n; ++i) + { + areaNodesK1[i] = nodes->getK1(area.nodeIds[i]); + areaNodesK2[i] = nodes->getK2(area.nodeIds[i]); + } + double var1 = MathOps::computeVariance(areaNodesK1); + double var2 = MathOps::computeVariance(areaNodesK2); + double minorCurvature; + if (var1 > var2) + minorCurvature = MathOps::mean(areaNodesK2); + else + minorCurvature = MathOps::mean(areaNodesK1); + area.minorRadius = 1.0 / minorCurvature; + std::vector majorRadiusNodes(3 * n, 0.0); + for (size_t i = 0; i < n; ++i) + { + std::array coords = nodes->getCoordinates(area.nodeIds[i]); + std::array normal = nodes->getNormal(area.nodeIds[i]); + for (size_t j = 0; j < 3; ++j) + majorRadiusNodes[3 * i + j] = coords[j] - normal[j] * area.minorRadius; + } + std::array meanMajorRadiusNodes = MathOps::meanCoordinates(majorRadiusNodes); + for (size_t i = 0; i < n; ++i) + { + for (size_t j = 0; j < 3; ++j) + majorRadiusNodes[3 * i + j] -= meanMajorRadiusNodes[j]; + } + std::array normal = MathOps::computePCAThirdAxis(majorRadiusNodes); + std::array base2d = MathOps::computeBaseFromNormal(normal); + std::vector projectedMajorRadiusNodes(2 * n, 0.0); + cblas_dgemm( + CBLAS_LAYOUT::CblasRowMajor, + CBLAS_TRANSPOSE::CblasNoTrans, + CBLAS_TRANSPOSE::CblasTrans, + (int)n, 2, 3, + 1.0, + majorRadiusNodes.data(), 3, + base2d.data(), 3, + 0.0, projectedMajorRadiusNodes.data(), 2); + std::vector A(3 * n, 1.0); + std::vector B(n, 0.0); + for (size_t i = 0; i < n; ++i) + { + for (size_t j = 0; j < 2; ++j) + A[3 * i + j] = projectedMajorRadiusNodes[2 * i + j]; + B[i] = pow(projectedMajorRadiusNodes[2 * i], 2) + + pow(projectedMajorRadiusNodes[2 * i + 1], 2); + } + std::vector fit = MathOps::lstsqRow(A, B); + double a = fit[0]; + double b = fit[1]; + double c = fit[2]; + double xc = a / 2.0; + double yc = b / 2.0; + area.radius = sqrt(4.0 * c + pow(a, 2) + pow(b, 2)) / 2.0; + for (size_t i = 0; i < 3; ++i) + area.center[i] = xc * base2d[i] + yc * base2d[3 + i] + meanMajorRadiusNodes[i]; +} + +const std::vector &Areas::getAreaIdByNodes() const +{ + return areaIdByNodes; +} diff --git a/src/ShapeRecogn/Areas.hxx b/src/ShapeRecogn/Areas.hxx new file mode 100644 index 000000000..b1b6f6bca --- /dev/null +++ b/src/ShapeRecogn/Areas.hxx @@ -0,0 +1,108 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __AREAS_HXX__ +#define __AREAS_HXX__ + +#include "PrimitiveType.hxx" +#include "Nodes.hxx" + +#include + +namespace MEDCoupling +{ + + struct Area + { + PrimitiveType primitive = PrimitiveType::Unknown; + double k1 = 0.0; + double k2 = 0.0; + double adimK1 = 0.0; + double adimK2 = 0.0; + double adimKdiff0 = 0.0; + double minorRadius = 0.0; + double radius = 0.0; + double angle = 0.0; + std::array normal{0.0, 0.0, 0.0}; + std::array center{0.0, 0.0, 0.0}; + std::array axis{0.0, 0.0, 0.0}; + std::array axisPoint{0.0, 0.0, 0.0}; + std::array apex{0.0, 0.0, 0.0}; + std::vector nodeIds; + }; + + class Areas + { + public: + Areas(const Nodes *nodes); + + mcIdType addArea(PrimitiveType primitive = PrimitiveType::Unknown); + void cleanArea(mcIdType areaId); + void cancelArea(mcIdType areaId, PrimitiveType primitive = PrimitiveType::Unknown); + void removeArea(mcIdType areaId); + void addNode(mcIdType areaId, mcIdType nodeId); + void cleanInvalidNodeAreas(); + + mcIdType getAreaId(mcIdType nodeId) const; + const std::vector &getAreaIdByNodes() const; + + bool isEmpty(mcIdType areaId) const; + size_t getNumberOfAreas() const; + size_t getNumberOfNodes(mcIdType areaId) const; + + bool isNodeCompatible(mcIdType areaId, mcIdType nodeId) const; + + PrimitiveType getPrimitiveType(mcIdType areaId) const; + std::string getPrimitiveTypeName(mcIdType areaId) const; + int getPrimitiveTypeInt(mcIdType areaId) const; + + const std::vector &getNodeIds(mcIdType areaId) const; + + double getAdimK1(mcIdType areaId) const; + double getAdimK2(mcIdType areaId) const; + double getAdimKdiff0(mcIdType areaId) const; + + void computeProperties(mcIdType areaId); + double getMinorRadius(mcIdType areaId) const; + double getRadius(mcIdType areaId) const; + double getAngle(mcIdType areaId) const; + const std::array &getNormal(mcIdType areaId) const; + const std::array &getCenter(mcIdType areaId) const; + const std::array &getAxis(mcIdType areaId) const; + const std::array &getAxisPoint(mcIdType areaId) const; + const std::array &getApex(mcIdType areaId) const; + std::array getAffinePoint(mcIdType areaId) const; + + private: + void cleanArea(mcIdType areaId, mcIdType newAreaId); + void removeNode(mcIdType nodeId); + + void computePlaneProperties(mcIdType areaId); + void computeSphereProperties(mcIdType areaId); + void computeCylinderProperties(mcIdType areaId); + void computeConeProperties(mcIdType areaId); + void computeTorusProperties(mcIdType areaId); + + std::vector areas; + const Nodes *nodes; + std::vector areaIdByNodes; + }; +}; + +#endif // __AREAS_HXX__ diff --git a/src/ShapeRecogn/AreasBuilder.cxx b/src/ShapeRecogn/AreasBuilder.cxx new file mode 100644 index 000000000..947a54ed0 --- /dev/null +++ b/src/ShapeRecogn/AreasBuilder.cxx @@ -0,0 +1,371 @@ +#include "AreasBuilder.hxx" +#include "MathOps.hxx" +#include "ShapeRecongConstants.hxx" + +#include + +using namespace MEDCoupling; + +AreasBuilder::AreasBuilder(const Nodes *nodes) : nodes(nodes), areas(new Areas(nodes)) +{ + size_t nbNodes = nodes->getNbNodes(); + threshold = std::max(THRESHOLD_MIN_NB_NODES, nbNodes / THRESHOLD_MAX_NB_AREAS); +} + +void AreasBuilder::build() +{ + explore(); + expand(); + rebuild(); +} + +void AreasBuilder::explore() +{ + exploreAreas(); + areas->cleanInvalidNodeAreas(); + filterHighPass(); +} + +void AreasBuilder::expand() +{ + expandAreas(); + filterHighPass(); +} + +void AreasBuilder::rebuild() +{ + rebuildInvalidAreas(); + filterHighPass(); + expandAreasByType(PrimitiveType::Cone); + filterHighPass(); +} + +Areas *AreasBuilder::getAreas() const +{ + return areas; +} + +void AreasBuilder::exploreAreas() +{ + // int nbNodesExplored = 0; + std::vector exploredNodeIds(nodes->getNbNodes(), false); + std::unordered_set nodesToExplore; + // Reserve a set with the size of nodes to avoid reallocation for each insert/erase + // TODO: Improve the size ? Nb of Nodes is too much ? + nodesToExplore.reserve(nodes->getNbNodes()); + mcIdType areaId = -1; + for (mcIdType nodeId = 0; nodeId < nodes->getNbNodes(); ++nodeId) + { + if (!exploredNodeIds[nodeId] && + nodes->getPrimitiveType(nodeId) != PrimitiveType::Unknown) + { + exploredNodeIds[nodeId] = true; + if (areaId != -1 && areas->getNumberOfNodes(areaId) < threshold) + areas->cancelArea(areaId, nodes->getPrimitiveType(nodeId)); + else + areaId = areas->addArea(nodes->getPrimitiveType(nodeId)); + areas->addNode(areaId, nodeId); + const std::vector neighbors = nodes->getNeighbors(nodeId); + for (mcIdType neighborId : neighbors) + { + if (nodes->getPrimitiveType(neighborId) == areas->getPrimitiveType(areaId) && + areas->getAreaId(neighborId) <= -1) + nodesToExplore.insert(neighborId); + } + // Explore all the neighbors matching the area + while (!nodesToExplore.empty()) + { + mcIdType neighborId = *nodesToExplore.begin(); + nodesToExplore.erase(neighborId); + if (doesItMatch(areaId, neighborId)) + { + exploredNodeIds[neighborId] = true; + areas->addNode(areaId, neighborId); + const std::vector neighborsOfNeighbor = nodes->getNeighbors(neighborId); + for (mcIdType neighborIdOfNeighbor : neighborsOfNeighbor) + { + if (!exploredNodeIds[neighborIdOfNeighbor] && + areas->getAreaId(neighborIdOfNeighbor) <= -1 && + // Already in doesItMatch but avoid useless insertion + nodes->getPrimitiveType(neighborIdOfNeighbor) == areas->getPrimitiveType(areaId)) + nodesToExplore.insert(neighborIdOfNeighbor); + } + } + } + } + // if (!exploredNodeIds[nodeId]) + // nbNodesExplored += 1; + exploredNodeIds[nodeId] = true; + } +} + +void AreasBuilder::expandAreas() +{ + // Expand by topological order + expandAreasByType(PrimitiveType::Plane); + expandAreasByType(PrimitiveType::Sphere); + expandAreasByType(PrimitiveType::Cylinder); + expandAreasByType(PrimitiveType::Cone); + expandAreasByType(PrimitiveType::Torus); +} + +void AreasBuilder::expandAreasByType(PrimitiveType primitive) +{ + std::unordered_set nodesToExplore; + // Reserve a set with the size of nodes to avoid reallocation for each insert/erase + // TODO: Improve the size ? Nb of Nodes is too much ? + nodesToExplore.reserve(nodes->getNbNodes()); + for (mcIdType areaId = 0; areaId < (mcIdType)areas->getNumberOfAreas(); ++areaId) + { + if (areas->getPrimitiveType(areaId) == primitive) + { + std::vector exploredNodeIds(nodes->getNbNodes(), false); + areas->computeProperties(areaId); + const std::vector &nodeIds = areas->getNodeIds(areaId); + for (mcIdType nodeId : nodeIds) + { + exploredNodeIds[nodeId] = true; + nodesToExplore.insert(nodeId); + } + while (!nodesToExplore.empty()) + { + mcIdType nodeId = *nodesToExplore.begin(); + nodesToExplore.erase(nodeId); + if (doesItBelong(areaId, nodeId)) + { + // TODO: Is the properties need to be updated after adding a node ? + // It gives bad results for the cone and the cylinder + // mcIdType oldAreaId = areas->getAreaId(nodeId); + areas->addNode(areaId, nodeId); + // areas->computeProperties(areaId); + // areas->computeProperties(oldAreaId); + const std::vector neighborIds = nodes->getNeighbors(nodeId); + for (mcIdType neighborId : neighborIds) + { + if (!exploredNodeIds[neighborId]) + nodesToExplore.insert(neighborId); + } + } + exploredNodeIds[nodeId] = true; + } + } + } +} + +void AreasBuilder::rebuildInvalidAreas() +{ + std::vector exploredNodeIds(nodes->getNbNodes(), false); + std::vector isIinvalidNodes(nodes->getNbNodes(), false); + std::unordered_set nodesToExplore; + // Reserve a set with the size of nodes to avoid reallocation for each insert/erase + // TODO: Improve the size ? Nb of Nodes is too much ? + nodesToExplore.reserve(nodes->getNbNodes()); + for (mcIdType nodeId = 0; nodeId < nodes->getNbNodes(); ++nodeId) + isIinvalidNodes[nodeId] = isInvalidCylinderNode(nodeId); + for (mcIdType nodeId = 0; nodeId < nodes->getNbNodes(); ++nodeId) + { + if (isIinvalidNodes[nodeId] && !exploredNodeIds[nodeId]) + { + mcIdType areaId = areas->addArea(PrimitiveType::Cone); + areas->addNode(areaId, nodeId); + const std::vector neighbors = nodes->getNeighbors(nodeId); + for (mcIdType neighborId : neighbors) + nodesToExplore.insert(neighborId); + while (!nodesToExplore.empty()) + { + mcIdType neighborId = *nodesToExplore.begin(); + nodesToExplore.erase(neighborId); + if ( + (areas->getAreaId(neighborId) == -1 || + isIinvalidNodes[neighborId])) + { + exploredNodeIds[neighborId] = true; + areas->addNode(areaId, neighborId); + const std::vector neighborsOfNeighbor = nodes->getNeighbors(neighborId); + for (mcIdType neighborIdOfNeighbor : neighborsOfNeighbor) + { + if (!exploredNodeIds[neighborIdOfNeighbor]) + nodesToExplore.insert(neighborIdOfNeighbor); + } + } + } + } + } +} + +void AreasBuilder::filterHighPass() +{ + mcIdType nbAreas = areas->getNumberOfAreas(); + for (mcIdType areaId = (nbAreas - 1); areaId >= 0; --areaId) + { + if (areas->getNumberOfNodes(areaId) < threshold) + areas->removeArea(areaId); + } +} + +bool AreasBuilder::doesItMatch(mcIdType areaId, mcIdType nodeId) const +{ + PrimitiveType areaType = areas->getPrimitiveType(areaId); + PrimitiveType neighborType = nodes->getPrimitiveType(nodeId); + bool isMatching = false; + if (areaType == neighborType) + { + switch (areaType) + { + case PrimitiveType::Plane: + case PrimitiveType::Torus: + isMatching = true; + break; + case PrimitiveType::Sphere: + { + double kmoy = fabs( + (areas->getAdimK1(areaId) + areas->getAdimK2(areaId)) / 2.0); + double nodeKmoy = fabs( + (nodes->getAdimK1(nodeId) + nodes->getAdimK2(nodeId)) / 2.0); + isMatching = fabs((nodeKmoy - kmoy)) < TOL_MATCH_SPHERE; + } + break; + case PrimitiveType::Cylinder: + isMatching = fabs(areas->getAdimKdiff0(areaId) - + nodes->getAdimKdiff0(nodeId)) < TOL_MATCH_CYLINDER; + break; + case PrimitiveType::Cone: + case PrimitiveType::Unknown: + default: + break; + } + } + return isMatching; +} + +bool AreasBuilder::doesItBelong(mcIdType areaId, mcIdType nodeId) const +{ + bool isClose = false; + if (areas->isNodeCompatible(areaId, nodeId)) + { + PrimitiveType areaType = areas->getPrimitiveType(areaId); + switch (areaType) + { + case PrimitiveType::Plane: + { + isClose = distanceToPlane( + nodes->getCoordinates(nodeId), + areas->getAffinePoint(areaId), + areas->getNormal(areaId)) < DELTA_PLANE; + } + break; + case PrimitiveType::Sphere: + { + double distanceToCenter = distanceToSphere( + nodes->getCoordinates(nodeId), + areas->getCenter(areaId)); + double radius = areas->getRadius(areaId); + isClose = fabs((distanceToCenter - radius) / radius) < DELTA_SPHERE; + } + break; + case PrimitiveType::Cylinder: + { + double distance = distanceToCylinder( + nodes->getCoordinates(nodeId), + areas->getAxis(areaId), + areas->getAxisPoint(areaId)); + double radius = areas->getRadius(areaId); + isClose = fabs((distance - radius) / radius) < DELTA_CYLINDER; + } + break; + case PrimitiveType::Cone: + { + double radius = areas->getRadius(areaId); + isClose = distanceToCone( + nodes->getCoordinates(nodeId), + areas->getAxis(areaId), + areas->getApex(areaId), + areas->getAngle(areaId)) / + fabs(radius) < + DELTA_CONE; + } + break; + case PrimitiveType::Torus: + case PrimitiveType::Unknown: + default: + break; + } + } + return isClose; +} + +bool AreasBuilder::isInvalidCylinderNode(mcIdType nodeId) const +{ + mcIdType areaId = areas->getAreaId(nodeId); + if (areaId != -1 && + nodes->getPrimitiveType(nodeId) == PrimitiveType::Cylinder && + areas->getPrimitiveType(areaId) == PrimitiveType::Cylinder) + { + double angle = MathOps::computeAngle( + nodes->getWeakDirection(nodeId), + areas->getAxis(areaId)); + return angle >= THETA_MAX_CYLINDER && angle <= (M_PI - THETA_MAX_CYLINDER); + } + return false; +} + +double AreasBuilder::distanceToPlane( + const std::array &nodeCoords, + const std::array &point, + const std::array &normal) +{ + std::array vec{0.0, 0.0, 0.0}; + for (size_t i = 0; i < nodeCoords.size(); ++i) + vec[i] = nodeCoords[i] - point[i]; + return fabs(MathOps::dot(vec, normal)) / MathOps::computeNorm(normal); +} + +double AreasBuilder::distanceToSphere( + const std::array &nodeCoords, + const std::array ¢er) +{ + return MathOps::computeNorm( + std::array{ + nodeCoords[0] - center[0], + nodeCoords[1] - center[1], + nodeCoords[2] - center[2]}); +} + +double AreasBuilder::distanceToCylinder( + const std::array &nodeCoords, + const std::array &axis, + const std::array &axisPoint) +{ + + std::array pa = { + axisPoint[0] - nodeCoords[0], + axisPoint[1] - nodeCoords[1], + axisPoint[2] - nodeCoords[2]}; + double innerProduct = MathOps::dot(pa, axis); + return MathOps::computeNorm( + std::array({pa[0] - innerProduct * axis[0], + pa[1] - innerProduct * axis[1], + pa[2] - innerProduct * axis[2]})); +} + +double AreasBuilder::distanceToCone( + const std::array &nodeCoords, + const std::array &axis, + const std::array &apex, + double angle) +{ + std::array ps{ + apex[0] - nodeCoords[0], + apex[1] - nodeCoords[1], + apex[2] - nodeCoords[2]}; + std::array v(axis); + if (MathOps::dot(axis, ps) <= 0) + { + for (size_t i = 0; i < 3; ++i) + v[i] *= -1; + } + double a = MathOps::computeNorm( + MathOps::cross(ps, v)); + double b = MathOps::dot(ps, v); + return fabs(a * cos(angle) - b * sin(angle)); +} diff --git a/src/ShapeRecogn/AreasBuilder.hxx b/src/ShapeRecogn/AreasBuilder.hxx new file mode 100644 index 000000000..9742af648 --- /dev/null +++ b/src/ShapeRecogn/AreasBuilder.hxx @@ -0,0 +1,74 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __AREASBUILDER_HXX__ +#define __AREASBUILDER_HXX__ + +#include "Nodes.hxx" +#include "Areas.hxx" + +namespace MEDCoupling +{ + class AreasBuilder + { + public: + AreasBuilder(const Nodes *nodes); + + void build(); + + Areas *getAreas() const; + + private: + void explore(); + void expand(); + void rebuild(); + void exploreAreas(); + void expandAreas(); + void expandAreasByType(PrimitiveType primitive); + void rebuildInvalidAreas(); + void filterHighPass(); + bool doesItMatch(mcIdType areaId, mcIdType nodeId) const; + bool doesItBelong(mcIdType areaId, mcIdType nodeId) const; + bool isInvalidCylinderNode(mcIdType nodeId) const; + + static double distanceToPlane( + const std::array &nodeCoords, + const std::array &point, + const std::array &normal); + static double distanceToSphere( + const std::array &nodeCoords, + const std::array ¢er); + static double distanceToCylinder( + const std::array &nodeCoords, + const std::array &axis, + const std::array &axisPoint); + static double distanceToCone( + const std::array &nodeCoords, + const std::array &axis, + const std::array &apex, + double angle); + + const Nodes *nodes; + Areas *areas; + + size_t threshold = 5; + }; +}; + +#endif // __AREASBUILDER_HXX__ \ No newline at end of file diff --git a/src/ShapeRecogn/CMakeLists.txt b/src/ShapeRecogn/CMakeLists.txt new file mode 100644 index 000000000..1f63ae163 --- /dev/null +++ b/src/ShapeRecogn/CMakeLists.txt @@ -0,0 +1,63 @@ +# Copyright (C) 2012-2024 CEA, EDF +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +ADD_DEFINITIONS(${HDF5_DEFINITIONS} ${MEDFILE_DEFINITIONS}) + +IF (NOT DEFINED MSVC) + ADD_DEFINITIONS(-Wsign-compare -Wconversion) +ENDIF() + +IF(MEDCOUPLING_ENABLE_PYTHON) + ADD_SUBDIRECTORY(Swig) +ENDIF(MEDCOUPLING_ENABLE_PYTHON) + +IF(MEDCOUPLING_BUILD_TESTS) + ADD_SUBDIRECTORY(Test) +ENDIF(MEDCOUPLING_BUILD_TESTS) + +INCLUDE_DIRECTORIES( + ${MEDFILE_INCLUDE_DIRS} + ${HDF5_INCLUDE_DIRS} + ${LAPACKE_INCLUDE_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR}/../MEDLoader + ${CMAKE_CURRENT_SOURCE_DIR}/../MEDCoupling + ${CMAKE_CURRENT_SOURCE_DIR}/../INTERP_KERNEL + ${CMAKE_CURRENT_SOURCE_DIR}/../INTERP_KERNEL/Bases +) + +SET(shaperecogn_SOURCES + MathOps.cxx + Nodes.cxx + NodesBuilder.cxx + Areas.cxx + AreasBuilder.cxx + ShapeRecognMeshBuilder.cxx + ShapeRecognMesh.cxx +) + +ADD_LIBRARY(shaperecogn ${shaperecogn_SOURCES}) +SET_TARGET_PROPERTIES(shaperecogn PROPERTIES COMPILE_FLAGS "") +TARGET_LINK_LIBRARIES(shaperecogn medcouplingcpp medloader ${MEDFILE_C_LIBRARIES} ${HDF5_LIBRARIES} ${LAPACK_LIBRARIES}) +INSTALL(TARGETS shaperecogn EXPORT ${PROJECT_NAME}TargetGroup DESTINATION ${MEDCOUPLING_INSTALL_LIBS}) + +FILE(GLOB shaperecogn_HEADERS_HXX "${CMAKE_CURRENT_SOURCE_DIR}/*.hxx") +INSTALL(FILES ${shaperecogn_HEADERS_HXX} DESTINATION ${MEDCOUPLING_INSTALL_HEADERS}) + +# To allow usage as SWIG dependencies: +SET(shaperecogn_HEADERS_HXX PARENT_SCOPE) diff --git a/src/ShapeRecogn/MathOps.cxx b/src/ShapeRecogn/MathOps.cxx new file mode 100644 index 000000000..8f1705ace --- /dev/null +++ b/src/ShapeRecogn/MathOps.cxx @@ -0,0 +1,331 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#include "MathOps.hxx" + +#include +#include +#include +#include +#include + +using namespace MEDCoupling; + +std::vector MathOps::lstsq( + std::vector &a, const std::vector &b) +{ + int m = (int)b.size(); + int n = 3; + int nrhs = 1; + return lstsq(a, b, m, n, nrhs); +} + +std::vector MathOps::lstsq( + std::vector &a, + const std::vector &b, + int m, int n, int nrhs) +{ + int ldb = std::max(m, n); + int lds = std::min(m, n); + std::vector x(ldb, 0.0); + for (size_t i = 0; i < b.size(); ++i) + x[i] = b[i]; + std::vector s(lds, 0.0); + double rcond = DBL_EPSILON * (double)ldb; // same value as numpy.linalg.lstsq + int rank = 0; + int info = LAPACKE_dgelsd( + LAPACK_COL_MAJOR, + m, n, nrhs, + a.data(), m, + x.data(), ldb, + s.data(), + rcond, + &rank); + return x; +} + +std::vector MathOps::lstsqRow( + std::vector &a, + const std::vector &b) +{ + int m = b.size(); + int n = 3; + int nrhs = 1; + int ldb = std::max(m, n); + int lds = std::min(m, n); + std::vector x(ldb, 0.0); + for (size_t i = 0; i < b.size(); ++i) + x[i] = b[i]; + std::vector s(lds, 0.0); + double rcond = DBL_EPSILON * (double)ldb; // same value as numpy.linalg.lstsq + int rank = 0; + int info = LAPACKE_dgelsd( + LAPACK_ROW_MAJOR, + m, n, nrhs, + a.data(), n, + x.data(), nrhs, + s.data(), + rcond, + &rank); + return x; +} +std::array MathOps::cross(const std::array &a, const std::array &b) +{ + std::array c{0.0, 0.0, 0.0}; + c[0] = a[1] * b[2] - a[2] * b[1]; + c[1] = a[2] * b[0] - a[0] * b[2]; + c[2] = a[0] * b[1] - a[1] * b[0]; + return c; +} + +std::array MathOps::normalize(const std::array &a) +{ + std::array an{0.0, 0.0, 0.0}; + double n = computeNorm(a); + for (size_t i = 0; i < 3; i++) + an[i] = a[i] / n; + return an; +} + +double MathOps::computeNorm(const std::array &a) +{ + double n = 0; + for (size_t i = 0; i < 3; i++) + n += a[i] * a[i]; + return sqrt(n); +} + +std::vector MathOps::computeNorm( + const std::vector &a) +{ + size_t s = a.size() / 3; + std::vector n(s, 0.0); + for (size_t i = 0; i < s; i++) + { + for (size_t j = 0; j < 3; j++) + n[i] += a[3 * i + j] * a[3 * i + j]; + n[i] = sqrt(n[i]); + } + return n; +} + +double MathOps::dot(const std::array &a, const std::array &b) +{ + double d = 0.0; + for (size_t i = 0; i < 3; i++) + d += a[i] * b[i]; + return d; +} + +std::vector MathOps::dot(const std::vector &a, const std::array &b) +{ + size_t nbNodes = a.size() / 3; + std::vector d(nbNodes, 0.0); + cblas_dgemv( + CBLAS_LAYOUT::CblasRowMajor, + CBLAS_TRANSPOSE::CblasNoTrans, + (int)nbNodes, 3, 1.0, + a.data(), 3, + b.data(), 1, + 0.0, d.data(), 1); + return d; +} + +double MathOps::mean(const std::vector &values) +{ + double mean = 0.0; + for (double value : values) + mean += value; + return mean / values.size(); +} + +std::array MathOps::meanCoordinates(const std::vector &coordinates) +{ + std::array coordsMean{0.0, 0.0, 0.0}; + size_t nbNodes = coordinates.size() / 3; + for (size_t nodeId = 0; nodeId < nbNodes; ++nodeId) + { + coordsMean[0] += coordinates[3 * nodeId]; + coordsMean[1] += coordinates[3 * nodeId + 1]; + coordsMean[2] += coordinates[3 * nodeId + 2]; + } + coordsMean[0] /= nbNodes; + coordsMean[1] /= nbNodes; + coordsMean[2] /= nbNodes; + return coordsMean; +} + +std::array MathOps::computeCov( + const std::vector &coordinates) +{ + std::array covMatrix; + covMatrix.fill(0); + size_t nbNodes = coordinates.size() / 3; + // Center the coordinates + std::vector coordsCentered(coordinates); + std::array coordsMean = meanCoordinates(coordinates); + for (size_t nodeId = 0; nodeId < nbNodes; ++nodeId) + { + coordsCentered[3 * nodeId] -= coordsMean[0]; + coordsCentered[3 * nodeId + 1] -= coordsMean[1]; + coordsCentered[3 * nodeId + 2] -= coordsMean[2]; + } + cblas_dgemm( + CBLAS_LAYOUT::CblasColMajor, + CBLAS_TRANSPOSE::CblasNoTrans, + CBLAS_TRANSPOSE::CblasTrans, + 3, 3, (int)nbNodes, 1.0, + coordsCentered.data(), 3, + coordsCentered.data(), 3, + 0.0, covMatrix.data(), 3); + for (size_t i = 0; i < 9; ++i) + { + covMatrix[i] /= (double)nbNodes - 1; + } + return covMatrix; +} + +std::array MathOps::computePCA(const std::vector &coordinates) +{ + std::array covMatrix = computeCov(coordinates); + std::array eigenValues{0.0, 0.0, 0.0}; + LAPACKE_dsyevd( + LAPACK_COL_MAJOR, + 'V', + 'U', + 3, + covMatrix.data(), + 3, + eigenValues.data()); + return covMatrix; +} + +std::array MathOps::computePCAFirstAxis(const std::vector &coordinates) +{ + std::array pca = computePCA(coordinates); + // The eignvalues are in ascending order so the first axis correspond to the last vector + return {pca[6], pca[7], pca[8]}; +} + +std::array MathOps::computePCAThirdAxis(const std::vector &coordinates) +{ + std::array pca = computePCA(coordinates); + // The eignvalues are in ascending order so the third axis correspond to the first vector + return {pca[0], pca[1], pca[2]}; +} + +double MathOps::computeQuantile(const std::vector &values, double q) +{ + std::vector sortedValues(values); + std::sort(sortedValues.begin(), sortedValues.end()); + double pos = q * (sortedValues.size() - 1); + size_t index = static_cast(pos); + if (pos == index) + return sortedValues[index]; + else + { + double frac = pos - index; + return sortedValues[index] * (1 - frac) + sortedValues[index + 1] * frac; + } +} + +double MathOps::computeAngle(const std::array &direction, std::array axis) +{ + double angle = dot(direction, axis); + double normAxis = computeNorm(axis); + double normDirection = computeNorm(direction); + angle /= normDirection * normAxis; + if (fabs(angle) >= 1) + return 0; + else + return acos(angle); +} + +std::vector MathOps::computeAngles( + const std::vector &directions, std::array axis) +{ + size_t nbDirections = directions.size() / 3; + std::vector angles(nbDirections, 0.0); + cblas_dgemv( + CBLAS_LAYOUT::CblasRowMajor, + CBLAS_TRANSPOSE::CblasNoTrans, + nbDirections, 3, 1.0, + directions.data(), 3, + axis.data(), 1, + 0.0, angles.data(), 1); + double normAxis = computeNorm(axis); + std::vector normDirections = computeNorm(directions); + for (size_t i = 0; i < nbDirections; ++i) + { + angles[i] /= normAxis * normDirections[i]; + if (fabs(angles[i]) >= 1.0) + angles[i] = 0.0; + angles[i] = acos(angles[i]); + } + return angles; +} + +double MathOps::computeOrientedAngle(const std::array &normal, const std::array &vector1, const std::array &vector2) +{ + double angle = computeAngle(vector1, vector2); + if (dot(cross(vector1, vector2), normal) >= 0.0) + return angle; + else + return -angle; +} + +double MathOps::computeVariance(std::vector values) +{ + size_t n = values.size(); + double m = mean(values); + double d2 = 0; + for (size_t i = 0; i < n; ++i) + d2 += pow(values[i] - m, 2); + return d2 / (double)n; +} + +std::array MathOps::computeBaseFromNormal(std::array normal) +{ + std::array n_normal = normalize(normal); + std::array s; + std::array u; + std::array v; + LAPACKE_dgesdd( + LAPACK_COL_MAJOR, + 'A', 1, 3, n_normal.data(), 1, + s.data(), + u.data(), 1, + v.data(), 3); + std::array v1 = {v[3], v[4], v[5]}; + std::array v2 = {v[6], v[7], v[8]}; + std::array u1 = normalize(v1); + std::array u2; + u2.fill(0.0); + double innerProd = dot(u1, v2); + for (size_t i = 0; i < 3; ++i) + u2[i] = v2[i] - innerProd * u1[i]; + u2 = normalize(u2); + double sign = dot(cross(u1, u2), normal); + if (sign < 0) + { + for (size_t i = 0; i < 3; ++i) + u1[i] *= -1; + } + return {u1[0], u1[1], u1[2], u2[0], u2[1], u2[2]}; +} diff --git a/src/ShapeRecogn/MathOps.hxx b/src/ShapeRecogn/MathOps.hxx new file mode 100644 index 000000000..e511845da --- /dev/null +++ b/src/ShapeRecogn/MathOps.hxx @@ -0,0 +1,71 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __MATHOPS_HXX__ +#define __MATHOPS_HXX__ + +#include +#include + +namespace MEDCoupling +{ + class MathOps + { + public: + static std::vector lstsq(std::vector &a, const std::vector &b); + static std::vector lstsq( + std::vector &a, + const std::vector &b, + int m, + int n = 3, + int nrhs = 1); + static std::vector lstsqRow( + std::vector &a, + const std::vector &b); + static std::array cross(const std::array &a, const std::array &b); + static std::array normalize(const std::array &a); + static double computeNorm(const std::array &a); + static std::vector computeNorm(const std::vector &a); + static double dot(const std::array &a, const std::array &b); + static std::vector dot(const std::vector &a, const std::array &b); + static double mean(const std::vector &values); + static std::array meanCoordinates(const std::vector &coordinates); + static std::array computeCov(const std::vector &coordinates); + static std::array computePCA(const std::vector &coordinates); + static std::array computePCAFirstAxis(const std::vector &coordinates); + static std::array computePCAThirdAxis(const std::vector &coordinates); + static double computeQuantile( + const std::vector &values, + double q); + static double computeAngle( + const std::array &direction, + std::array axis); + static std::vector computeAngles( + const std::vector &directions, + std::array axis); + static double computeOrientedAngle( + const std::array &normal, + const std::array &vector1, + const std::array &vector2); + static double computeVariance(std::vector values); + static std::array computeBaseFromNormal(std::array normal); + }; +}; + +#endif //__MATHOPS_HXX__ diff --git a/src/ShapeRecogn/Nodes.cxx b/src/ShapeRecogn/Nodes.cxx new file mode 100644 index 000000000..615971ad4 --- /dev/null +++ b/src/ShapeRecogn/Nodes.cxx @@ -0,0 +1,125 @@ +#include "Nodes.hxx" + +using namespace MEDCoupling; + +Nodes::Nodes( + const MEDCouplingUMesh *mesh, + const DataArrayInt64 *neighbors, + const DataArrayInt64 *neighborsIdx) + : mesh(mesh), coords(mesh->getCoords()), neighbors(neighbors), neighborsIdx(neighborsIdx) +{ +} + +Nodes::~Nodes() +{ + mesh->decrRef(); + neighbors->decrRef(); + neighborsIdx->decrRef(); +} + +mcIdType Nodes::getNbNodes() const +{ + return coords->getNumberOfTuples(); +} + +const std::vector &Nodes::getK1() const +{ + return k1; +} + +const std::vector &Nodes::getK2() const +{ + return k2; +} + +const std::vector &Nodes::getPrimitiveType() const +{ + return primitives; +} + +const std::vector &Nodes::getNormals() const +{ + return normals; +} + +const std::vector &Nodes::getWeakDirections() const +{ + return weakDirections; +} + +const std::vector &Nodes::getMainDirections() const +{ + return mainDirections; +} + +std::array Nodes::getNormal(mcIdType nodeId) const +{ + return {normals[3 * nodeId], normals[3 * nodeId + 1], normals[3 * nodeId + 2]}; +} + +double Nodes::getK1(mcIdType nodeId) const +{ + return k1[nodeId]; +} + +double Nodes::getK2(mcIdType nodeId) const +{ + return k2[nodeId]; +} + +double Nodes::getKdiff0(mcIdType nodeId) const +{ + return fabs(k1[nodeId]) > fabs(k2[nodeId]) ? k1[nodeId] : k2[nodeId]; +} + +double Nodes::getAdimK1(mcIdType nodeId) const +{ + return adimK1[nodeId]; +} + +double Nodes::getAdimK2(mcIdType nodeId) const +{ + return adimK2[nodeId]; +} + +double Nodes::getAdimKdiff0(mcIdType nodeId) const +{ + return fabs(adimK1[nodeId]) > fabs(adimK2[nodeId]) ? adimK1[nodeId] : adimK2[nodeId]; +} + +const std::vector Nodes::getNeighbors(mcIdType nodeId) const +{ + mcIdType start = neighborsIdx->getIJ(nodeId, 0); + mcIdType nbNeighbors = neighborsIdx->getIJ(nodeId + 1, 0) - start; + + std::vector neighborNodes(nbNeighbors, 0); + for (mcIdType i = 0; i < nbNeighbors; i++) + neighborNodes[i] = neighbors->getIJ(start + i, 0); + return neighborNodes; +} + +PrimitiveType Nodes::getPrimitiveType(mcIdType nodeId) const +{ + return primitives[nodeId]; +} + +std::array Nodes::getWeakDirection(mcIdType nodeId) const +{ + return {weakDirections[3 * nodeId], + weakDirections[3 * nodeId + 1], + weakDirections[3 * nodeId + 2]}; +} + +std::array Nodes::getMainDirection(mcIdType nodeId) const +{ + return {mainDirections[3 * nodeId], + mainDirections[3 * nodeId + 1], + mainDirections[3 * nodeId + 2]}; +} + +std::array Nodes::getCoordinates(mcIdType nodeId) const +{ + std::array nodeCoords; + coords->getTuple(nodeId, nodeCoords.data()); + return nodeCoords; +} diff --git a/src/ShapeRecogn/Nodes.hxx b/src/ShapeRecogn/Nodes.hxx new file mode 100644 index 000000000..1d47d3060 --- /dev/null +++ b/src/ShapeRecogn/Nodes.hxx @@ -0,0 +1,78 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __NODES_HXX__ +#define __NODES_HXX__ + +#include +#include "MEDCouplingUMesh.hxx" +#include "PrimitiveType.hxx" + +namespace MEDCoupling +{ + class Nodes + { + public: + friend class NodesBuilder; + Nodes(const MEDCouplingUMesh *mesh, + const DataArrayInt64 *neighbors, + const DataArrayInt64 *neighborsIdx); + ~Nodes(); + + mcIdType getNbNodes() const; + const std::vector &getK1() const; + const std::vector &getK2() const; + const std::vector &getPrimitiveType() const; + const std::vector &getNormals() const; + const std::vector &getWeakDirections() const; + const std::vector &getMainDirections() const; + + std::array getNormal(mcIdType nodeId) const; + double getK1(mcIdType nodeId) const; + double getK2(mcIdType nodeId) const; + double getKdiff0(mcIdType nodeId) const; + double getAdimK1(mcIdType nodeId) const; + double getAdimK2(mcIdType nodeId) const; + double getAdimKdiff0(mcIdType nodeId) const; + const std::vector getNeighbors(mcIdType nodeId) const; + PrimitiveType getPrimitiveType(mcIdType nodeId) const; + std::array getWeakDirection(mcIdType nodeId) const; + std::array getMainDirection(mcIdType nodeId) const; + std::array getCoordinates(mcIdType nodeId) const; + + private: + const MEDCouplingUMesh *mesh; + const DataArrayDouble *coords; + // normals 3 * nbNodes + std::vector normals; + // neighbors + const DataArrayInt64 *neighbors; + const DataArrayInt64 *neighborsIdx; + // curvature + std::vector k1; + std::vector k2; + std::vector adimK1; + std::vector adimK2; + std::vector weakDirections; + std::vector mainDirections; + std::vector primitives; + }; +}; + +#endif //__NODES_HXX__ diff --git a/src/ShapeRecogn/NodesBuilder.cxx b/src/ShapeRecogn/NodesBuilder.cxx new file mode 100644 index 000000000..1302d3203 --- /dev/null +++ b/src/ShapeRecogn/NodesBuilder.cxx @@ -0,0 +1,311 @@ + +#include "NodesBuilder.hxx" +#include "Nodes.hxx" +#include "MathOps.hxx" +#include "ShapeRecongConstants.hxx" + +using namespace MEDCoupling; + +NodesBuilder::NodesBuilder( + const MEDCouplingUMesh *mesh) : mesh(mesh) +{ +} + +Nodes *NodesBuilder::build() +{ + DataArrayInt64 *neighbors; + DataArrayInt64 *neighborsIdx; + mesh->computeNeighborsOfNodes(neighbors, neighborsIdx); + nodes = new Nodes(mesh, neighbors, neighborsIdx); + computeNormals(); + computeCurvatures(); + return nodes; +} + +void NodesBuilder::computeNormals() +{ + mcIdType nbNodes = mesh->getNumberOfNodes(); + mcIdType nbCells = mesh->getNumberOfCells(); + std::array cellNormal; + cellNormal.fill(0.0); + // product of normal AreaByCell + std::vector prodNormalAreaByCell(3 * nbCells, 0.0); + for (int cellId = 0; cellId < nbCells; cellId++) + { + std::vector nodeIds; + mesh->getNodeIdsOfCell(cellId, nodeIds); + computeCellNormal(nodeIds, cellNormal); + prodNormalAreaByCell[3 * cellId + 0] = cellNormal[0] / 2.0; + prodNormalAreaByCell[3 * cellId + 1] = cellNormal[1] / 2.0; + prodNormalAreaByCell[3 * cellId + 2] = cellNormal[2] / 2.0; + } + // + nodes->normals.resize(3 * nbNodes, 1.0); + DataArrayInt64 *revNodal = DataArrayInt64::New(); + DataArrayInt64 *revNodalIdx = DataArrayInt64::New(); + mesh->getReverseNodalConnectivity(revNodal, revNodalIdx); + for (size_t nodeId = 0; nodeId < (size_t)nbNodes; nodeId++) + { + int nbCells = revNodalIdx->getIJ(nodeId + 1, 0) - + revNodalIdx->getIJ(nodeId, 0); + std::vector cellIds(nbCells, 0); + int start = revNodalIdx->getIJ(nodeId, 0); + for (size_t i = 0; i < cellIds.size(); ++i) + cellIds[i] = revNodal->getIJ(start + i, 0); + double normal = 0.0; + for (size_t i = 0; i < 3; i++) + { + nodes->normals[3 * nodeId + i] = 0.0; + for (mcIdType j = 0; j < nbCells; j++) + { + nodes->normals[3 * nodeId + i] += + prodNormalAreaByCell[3 * cellIds[j] + i]; + } + nodes->normals[3 * nodeId + i] /= (double)nbCells; + normal += pow(nodes->normals[3 * nodeId + i], 2); + } + for (size_t i = 0; i < 3; i++) + nodes->normals[3 * nodeId + i] /= sqrt(normal); + } + revNodal->decrRef(); + revNodalIdx->decrRef(); +} + +void NodesBuilder::computeCurvatures(double tol) +{ + mcIdType nbNodes = mesh->getNumberOfNodes(); + nodes->k1.resize(nbNodes); + nodes->k2.resize(nbNodes); + nodes->adimK1.resize(nbNodes); + nodes->adimK2.resize(nbNodes); + nodes->primitives.resize(nbNodes); + nodes->weakDirections.resize(3 * nbNodes); + nodes->mainDirections.resize(3 * nbNodes); + for (mcIdType nodeId = 0; nodeId < nbNodes; nodeId++) + computeCurvatures(nodeId, tol); +} + +void NodesBuilder::computeCurvatures(mcIdType nodeId, double tol) +{ + std::array normal = nodes->getNormal(nodeId); + std::vector neighborIds = nodes->getNeighbors(nodeId); + double theta0 = 0.0; + double k1 = 0.0; + double k2 = 0.0; + double adimK1 = 0.0; + double adimK2 = 0.0; + PrimitiveType primitive = PrimitiveType::Unknown; + std::array mainDirection{0.0, 0.0, 0.0}; + std::array weakDirection{0.0, 0.0, 0.0}; + if (neighborIds.size() > 0) + { + std::vector discreteCurvatures = computeDiscreteCurvatures(nodeId, neighborIds); + std::vector tangents = computeTangentDirections(nodeId, neighborIds); + mcIdType maxCurvatureId = std::distance( + discreteCurvatures.begin(), + std::max_element(discreteCurvatures.begin(), discreteCurvatures.end())); + std::array e1{ + tangents[3 * maxCurvatureId], + tangents[3 * maxCurvatureId + 1], + tangents[3 * maxCurvatureId + 2]}; + std::array e2 = MathOps::normalize(MathOps::cross(e1, normal)); + std::vector coeffs = computeNormalCurvatureCoefficients( + discreteCurvatures, tangents, normal, e1); + double a = coeffs[0], b = coeffs[1], c = coeffs[2]; + double h = (a + c) / 2.0; + double kg = a * c - pow(b, 2) / 4.0; + k1 = h + sqrt(fabs(pow(h, 2) - kg)); + k2 = h - sqrt(fabs(pow(h, 2) - kg)); + if (fabs(k1 - k2) >= tol) + theta0 = 0.5 * asin(b / (k1 - k2)); + std::array direction1{0.0, 0.0, 0.0}; + std::array direction2{0.0, 0.0, 0.0}; + for (size_t i = 0; i < 3; ++i) + { + direction1[i] = cos(theta0) * e1[i] + sin(theta0) * e2[i]; + direction2[i] = -sin(theta0) * e1[i] + cos(theta0) * e2[i]; + } + double averageDistance = computeAverageDistance(nodeId, neighborIds); + double adimK1 = k1 * averageDistance; + double adimK2 = k2 * averageDistance; + double adimKdiff0, adimKis0; + if (fabs(k1) > fabs(k2)) + { + adimKdiff0 = adimK1; + adimKis0 = adimK2; + mainDirection = direction1; + weakDirection = direction2; + } + else + { + adimKdiff0 = adimK2; + adimKis0 = adimK1; + mainDirection = direction2; + weakDirection = direction1; + } + primitive = findPrimitiveType(adimK1, adimK2, adimKdiff0, adimKis0); + } + // Populate nodes + nodes->k1[nodeId] = k1; + nodes->k2[nodeId] = k2; + nodes->adimK1[nodeId] = adimK1; + nodes->adimK2[nodeId] = adimK2; + for (size_t i = 0; i < 3; ++i) + { + nodes->weakDirections[3 * nodeId + i] = weakDirection[i]; + nodes->mainDirections[3 * nodeId + i] = mainDirection[i]; + } + nodes->primitives[nodeId] = primitive; +} + +PrimitiveType NodesBuilder::findPrimitiveType(double k1, double k2, double kdiff0, double kis0) const +{ + if ((fabs(k1 - k2) < EPSILON_PRIMITIVE) && (fabs((k1 + k2) / 2) < EPSILON_PRIMITIVE)) + return PrimitiveType::Plane; + else if ((fabs(k1 - k2) < EPSILON_PRIMITIVE) && (fabs((k1 + k2) / 2) > EPSILON_PRIMITIVE)) + return PrimitiveType::Sphere; + else if ((fabs(kdiff0) > EPSILON_PRIMITIVE) && (fabs(kis0) < EPSILON_PRIMITIVE)) + return PrimitiveType::Cylinder; + else if ((fabs(k1) > EPSILON_PRIMITIVE) && (fabs(k2) > EPSILON_PRIMITIVE)) + return PrimitiveType::Torus; + else + return PrimitiveType::Unknown; +} + +PrimitiveType NodesBuilder::findPrimitiveType2(double k1, double k2, double kdiff0, double kis0) const +{ + double epsilon2 = pow(EPSILON_PRIMITIVE, 2); + double diffCurvature = fabs(k1 - k2); + double gaussianCurvature = k1 * k2; + double meanCurvature = (k1 + k2) / 2.0; + if (fabs(k1) < EPSILON_PRIMITIVE && + fabs(k2) < EPSILON_PRIMITIVE && + gaussianCurvature < epsilon2 && + meanCurvature < EPSILON_PRIMITIVE) + return PrimitiveType::Plane; + else if (diffCurvature < EPSILON_PRIMITIVE && k1 > EPSILON_PRIMITIVE && k2 > EPSILON_PRIMITIVE) + return PrimitiveType::Sphere; + else if ( + (fabs(k1) > EPSILON_PRIMITIVE && fabs(k2) < EPSILON_PRIMITIVE) || + (fabs(k1) < EPSILON_PRIMITIVE && fabs(k2) > EPSILON_PRIMITIVE)) + return PrimitiveType::Cylinder; + else if ( + std::signbit(k1) != std::signbit(k2) || + (fabs(k1) < EPSILON_PRIMITIVE && fabs(k2) < EPSILON_PRIMITIVE)) + return PrimitiveType::Torus; + else + return PrimitiveType::Unknown; +} + +std::vector NodesBuilder::computeNormalCurvatureCoefficients( + const std::vector &discreteCurvatures, + const std::vector &tangents, + const std::array &normal, + const std::array &e1) const +{ + size_t nbNeighbors = discreteCurvatures.size(); + std::vector a(3 * nbNeighbors, 0.0); + for (size_t i = 0; i < nbNeighbors; ++i) + { + std::array tangent{tangents[3 * i], tangents[3 * i + 1], tangents[3 * i + 2]}; + double theta = MathOps::computeOrientedAngle(normal, tangent, e1); + double cos_theta = cos(theta); + double sin_theta = sin(theta); + a[i] = pow(cos_theta, 2); + a[nbNeighbors + i] = cos_theta * sin_theta; + a[2 * nbNeighbors + i] = pow(sin_theta, 2); + } + return MathOps::lstsq(a, discreteCurvatures); +} + +void NodesBuilder::computeCellNormal( + const std::vector &nodeIds, + std::array &cellNormal) const +{ + std::vector point1; + std::vector point2; + std::vector point3; + mesh->getCoordinatesOfNode(nodeIds[0], point1); + mesh->getCoordinatesOfNode(nodeIds[1], point2); + mesh->getCoordinatesOfNode(nodeIds[2], point3); + std::array a; + a.fill(3); + std::array b; + b.fill(3); + for (int i = 0; i < 3; i++) + { + a[i] = point2[i] - point1[i]; + b[i] = point3[i] - point1[i]; + } + cellNormal[0] = a[1] * b[2] - a[2] * b[1]; + cellNormal[1] = a[2] * b[0] - a[0] * b[2]; + cellNormal[2] = a[0] * b[1] - a[1] * b[0]; +} + +double NodesBuilder::computeAverageDistance(mcIdType nodeId, const std::vector &neighborIds) const +{ + double distance = 0.0; + std::array + nodeCoords = nodes->getCoordinates(nodeId); + for (size_t i = 0; i < neighborIds.size(); ++i) + { + std::array neighborCoords = nodes->getCoordinates(neighborIds[i]); + double distanceToNeighbor = 0.0; + for (size_t j = 0; j < 3; ++j) + distanceToNeighbor += pow(neighborCoords[j] - nodeCoords[j], 2); + distance += sqrt(distanceToNeighbor); + } + return distance / (double)neighborIds.size(); +} + +std::vector NodesBuilder::computeDiscreteCurvatures( + mcIdType nodeId, + const std::vector &neighborIds) const +{ + std::vector discreteCurvatures(neighborIds.size(), 0.0); + for (size_t i = 0; i < neighborIds.size(); ++i) + discreteCurvatures[i] = computeDiscreteCurvature(nodeId, neighborIds[i]); + return discreteCurvatures; +} + +double NodesBuilder::computeDiscreteCurvature( + mcIdType nodeId, + mcIdType neighborId) const +{ + double curvature = 0.0; + double n = 0.0; + for (size_t i = 0; i < 3; i++) + { + double ni = nodes->coords->getIJ(neighborId, i) - nodes->coords->getIJ(nodeId, i); + curvature += ni * (nodes->normals[3 * neighborId + i] - nodes->normals[3 * nodeId + i]); + n += ni * ni; + } + return curvature / n; +} + +std::vector NodesBuilder::computeTangentDirections( + mcIdType nodeId, + const std::vector &neighborIds) const +{ + size_t nbNeighbors = neighborIds.size(); + std::vector tangent(3 * nbNeighbors, 0.0); + for (size_t i = 0; i < nbNeighbors; ++i) + { + mcIdType neighborId = neighborIds[i]; + double s = 0.0; + for (size_t j = 0; j < 3; j++) + { + tangent[3 * i + j] = nodes->coords->getIJ(neighborId, j) - nodes->coords->getIJ(nodeId, j); + s += tangent[3 * i + j] * nodes->normals[3 * nodeId + j]; + } + double n = 0.0; + for (size_t j = 0; j < 3; j++) + { + tangent[3 * i + j] -= s * nodes->normals[3 * nodeId + j]; + n += tangent[3 * i + j] * tangent[3 * i + j]; + } + for (size_t j = 0; j < 3; j++) + tangent[3 * i + j] /= sqrt(n); + } + return tangent; +} diff --git a/src/ShapeRecogn/NodesBuilder.hxx b/src/ShapeRecogn/NodesBuilder.hxx new file mode 100644 index 000000000..190118064 --- /dev/null +++ b/src/ShapeRecogn/NodesBuilder.hxx @@ -0,0 +1,62 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __NODECURVATURECALCULATOR_HXX__ +#define __NODECURVATURECALCULATOR_HXX__ + +#include +#include +#include +#include "MEDCouplingUMesh.hxx" +#include "PrimitiveType.hxx" + +namespace MEDCoupling +{ + class Nodes; + + class NodesBuilder + { + public: + NodesBuilder(const MEDCouplingUMesh *); + + Nodes *build(); + + private: + void computeNormals(); + void computeCurvatures(double tol = 0.000001); + void computeCurvatures(mcIdType nodeId, double tol); + PrimitiveType findPrimitiveType(double k1, double k2, double kdiff0, double kis0) const; + PrimitiveType findPrimitiveType2(double k1, double k2, double kdiff0, double kis0) const; + std::vector computeNormalCurvatureCoefficients( + const std::vector &discreteCurvatures, + const std::vector &tangents, + const std::array &normal, + const std::array &e1) const; + void computeCellNormal(const std::vector &nodeIds, std::array &cellNormal) const; + double computeAverageDistance(mcIdType nodeId, const std::vector &neighborIds) const; + std::vector computeDiscreteCurvatures(mcIdType nodeId, const std::vector &neighborIds) const; + double computeDiscreteCurvature(mcIdType nodeId, mcIdType neighborId) const; + std::vector computeTangentDirections(mcIdType nodeId, const std::vector &neighborIds) const; + + const MEDCouplingUMesh *mesh; + Nodes *nodes; + }; +}; + +#endif //__NODECURVATURECALCULATOR_HXX__ \ No newline at end of file diff --git a/src/ShapeRecogn/PrimitiveType.hxx b/src/ShapeRecogn/PrimitiveType.hxx new file mode 100644 index 000000000..b131da84a --- /dev/null +++ b/src/ShapeRecogn/PrimitiveType.hxx @@ -0,0 +1,93 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __PRIMITIVETYPE_HXX__ +#define __PRIMITIVETYPE_HXX__ + +#include +namespace MEDCoupling +{ + enum PrimitiveType + { + Plane = 0, + Sphere = 1, + Cylinder = 2, + Cone = 3, + Torus = 4, + Unknown = 5 + }; + + inline std::string convertPrimitiveToString(PrimitiveType type) + { + std::string typeName = ""; + switch (type) + { + case PrimitiveType::Plane: + typeName = "Plane"; + break; + case PrimitiveType::Sphere: + typeName = "Sphere"; + break; + case PrimitiveType::Cylinder: + typeName = "Cylinder"; + break; + case PrimitiveType::Cone: + typeName = "Cone"; + break; + case PrimitiveType::Torus: + typeName = "Torus"; + break; + case PrimitiveType::Unknown: + typeName = "Unknown"; + break; + default: + break; + } + return typeName; + }; + + inline int convertPrimitiveToInt(PrimitiveType type) + { + int typeInt = 5; + switch (type) + { + case PrimitiveType::Plane: + typeInt = 0; + break; + case PrimitiveType::Sphere: + typeInt = 1; + break; + case PrimitiveType::Cylinder: + typeInt = 2; + break; + case PrimitiveType::Cone: + typeInt = 3; + break; + case PrimitiveType::Torus: + typeInt = 4; + break; + case PrimitiveType::Unknown: + default: + break; + } + return typeInt; + }; +}; + +#endif // __PRIMITIVETYPE_HXX__ \ No newline at end of file diff --git a/src/ShapeRecogn/README.md b/src/ShapeRecogn/README.md new file mode 100644 index 000000000..61b9eb4d0 --- /dev/null +++ b/src/ShapeRecogn/README.md @@ -0,0 +1,106 @@ +# ShapeRecogn +A tool leveraging the MEDCoupling library for recognizing canonical shapes in 3D mesh files. + +## Table Of Contents + +1. [Quickstart](#quickstart) +2. [Installation](#installation) +3. [Description](#description) + +## Quickstart + +### With output file + +```python +>> import ShapeRecogn as sr +>> +>> shape_recogn_builder = sr.ShapeRecognMeshBuilder("resources/ShapeRecognCone.med") +>> shape_recogn = shape_recogn_builder.recognize() +>> shape_recogn.save("ShapeRecognCone_areas.med") +``` + +### Without output file + +```python +>> import ShapeRecogn as sr +>> +>> shape_recogn_builder = sr.ShapeRecognMeshBuilder("resources/ShapeRecognCone.med") +>> shape_recogn = shape_recogn_builder.recognize() +>> radius_field = shape_recogn.getRadius() +>> radius_field +MEDCouplingFieldDouble C++ instance at 0x55f3418c6700. Name : "Radius (Area)". +Nature of field : NoNature. +P1 spatial discretization. + +Mesh info : MEDCouplingUMesh C++ instance at 0x55f34171a2d0. Name : "Mesh_1". Mesh dimension : 2. Space dimension : 3. + +Array info : DataArrayDouble C++ instance at 0x55f3418b40e0. Number of tuples : 957. Number of components : 1. +[9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, 9.8948383131651862, ... ] +``` + +## Installation + +### Prerequisites + +**LAPACKE Library**: ShapeRecogn requires the [LAPACKE](https://www.netlib.org/lapack/lapacke.html) library for numerical computations. Ensure it is installed on your system. + +### CMAKE configuration + +Run CMake to configure the build. Make sure to enable ShapeRecogn by setting the `-DMEDCOUPLING_ENABLE_SHAPERECOGN=ON` option. + +## Description + +ShapeRecogn is a tool that leverages the MEDCoupling library to recognize +canonical shapes from 3D meshes using triangle cells. + +The tool is based on the thesis work of Roseline Bénière *[Reconstruction d’un modèle B-Rep à partir d’un maillage 3D](https://theses.fr/2012MON20034)*, and it recognizes five canonical shapes: + - Plane (0) + - Sphere (1) + - Cylinder (2) + - Cone (3) + - Torus (4) + - Unknown (5) (When the algorithm failed the recognition) + +The recognized shapes are divided into areas within the mesh. +The tool also generates an output file with detailed fields describing the areas and their properties. This makes it easier to analyze or further process the mesh data. + - Area Id: To distinguish the areas + - Primitive Type (Area): One of the canonical shape with the id describe above + - Normal (Area): + * Normal of a plane area + - Minor Radius (Area) + * Minor radius of a torus area + - Radius (Area) + * Radius of a sphere area + * Radius of a cylinder area + * Radius of the base of a cone area + * Major radius of a torus area + - Angle (Area) + * Angle between the central axis and the slant of a cone area + - Center (Area) + * Center of a sphere area + * Center of a torus area + - Axis (Area) + * Central axis of a cylinder area + * Central axis of a cone area + - Apex (Area) + * Apex of a cone area + +Some intermediate computation values concerning the nodes are also available: + - K1 (Node) and K2 (Node): the *[principal curvatures](https://en.wikipedia.org/wiki/Principal_curvature)* + - Primitive Type (Node) : One of the canonical shape with the id describe above. The primitive type is deduced usint K1 and K2 values. + - Normal (Node): Normal of the nodes using neighbor nodes + +Each field can be retrieved as a `MEDCouplingDoubleField` using the following methods of the `ShapeRecognMesh` class: + - `getAreaId()` + - `getAreaPrimitiveType()` + - `getAreaNormal()` + - `getMinorRadius()` + - `getRadius()` + - `getAngle()` + - `getCenter()` + - `getAxis()` + - `getApex()` + - `getNodeK1()` + - `getNodeK2()` + - `getNodePrimitiveType()` + - `getNodeNormal()` diff --git a/src/ShapeRecogn/ShapeRecognMesh.cxx b/src/ShapeRecogn/ShapeRecognMesh.cxx new file mode 100644 index 000000000..4c8ee5f9f --- /dev/null +++ b/src/ShapeRecogn/ShapeRecognMesh.cxx @@ -0,0 +1,155 @@ +#include "ShapeRecognMesh.hxx" + +#include "MEDLoader.hxx" + +using namespace MEDCoupling; + +ShapeRecognMesh::ShapeRecognMesh() + : nodeK1(0), nodeK2(0), nodePrimitiveType(0), + nodeNormal(0), areaId(0), areaPrimitiveType(0), + areaNormal(0), minorRadius(0), radius(0), + angle(0), center(0), axis(0), apex(0) +{ +} +ShapeRecognMesh::~ShapeRecognMesh() +{ + nodeK1->decrRef(); + nodeK2->decrRef(); + nodePrimitiveType->decrRef(); + nodeNormal->decrRef(); + areaId->decrRef(); + areaPrimitiveType->decrRef(); + areaNormal->decrRef(); + minorRadius->decrRef(); + radius->decrRef(); + angle->decrRef(); + center->decrRef(); + axis->decrRef(); + apex->decrRef(); +} + +std::size_t ShapeRecognMesh::getHeapMemorySizeWithoutChildren() const +{ + return 0; +} + +std::vector ShapeRecognMesh::getDirectChildrenWithNull() const +{ + std::vector ret; + ret.push_back(nodeK1); + ret.push_back(nodeK2); + ret.push_back(nodePrimitiveType); + ret.push_back(nodeNormal); + ret.push_back(areaId); + ret.push_back(areaPrimitiveType); + ret.push_back(areaNormal); + ret.push_back(minorRadius); + ret.push_back(radius); + ret.push_back(angle); + ret.push_back(center); + ret.push_back(axis); + ret.push_back(apex); + return ret; +} + +ShapeRecognMesh *ShapeRecognMesh::New() +{ + return new ShapeRecognMesh; +} + +void ShapeRecognMesh::save(const std::string &outputFile, bool writeFromScratch) const +{ + // Nodes + // - k1 + WriteField(outputFile, nodeK1, writeFromScratch); + // - k2 + WriteField(outputFile, nodeK2, false); + // - primitive types + WriteField(outputFile, nodePrimitiveType, false); + // - Normal + WriteField(outputFile, nodeNormal, false); + // Areas + // - Area Id + WriteField(outputFile, areaId, false); + // - Primitive Types + WriteField(outputFile, areaPrimitiveType, false); + // - Normal + WriteField(outputFile, areaNormal, false); + // - Minor Radius + WriteField(outputFile, minorRadius, false); + // - Radius + WriteField(outputFile, radius, false); + // - Angle + WriteField(outputFile, angle, false); + // - Center + WriteField(outputFile, center, false); + // - Axis + WriteField(outputFile, axis, false); + // - Apex + WriteField(outputFile, apex, false); +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getNodeK1() const +{ + return nodeK1; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getNodeK2() const +{ + return nodeK2; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getNodePrimitiveType() const +{ + return nodePrimitiveType; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getNodeNormal() const +{ + return nodeNormal; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getAreaId() const +{ + return areaId; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getAreaPrimitiveType() const +{ + return areaPrimitiveType; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getAreaNormal() const +{ + return areaNormal; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getMinorRadius() const +{ + return minorRadius; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getRadius() const +{ + return radius; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getAngle() const +{ + return angle; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getCenter() const +{ + return center; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getAxis() const +{ + return axis; +} + +MEDCouplingFieldDouble *ShapeRecognMesh::getApex() const +{ + return apex; +} diff --git a/src/ShapeRecogn/ShapeRecognMesh.hxx b/src/ShapeRecogn/ShapeRecognMesh.hxx new file mode 100644 index 000000000..02983e34a --- /dev/null +++ b/src/ShapeRecogn/ShapeRecognMesh.hxx @@ -0,0 +1,80 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __SHAPERECOGNMESH_HXX__ +#define __SHAPERECOGNMESH_HXX__ + +#include + +#include "MEDCouplingUMesh.hxx" +#include "MEDCouplingFieldDouble.hxx" +#include "MEDCouplingRefCountObject.hxx" + +namespace MEDCoupling +{ + class ShapeRecognMesh : public RefCountObject + { + friend class ShapeRecognMeshBuilder; + + public: + static ShapeRecognMesh *New(); + std::size_t getHeapMemorySizeWithoutChildren() const; + std::vector getDirectChildrenWithNull() const; + + void save(const std::string &outputFile, bool writeFromScratch = true) const; + + // Node properties + MEDCoupling::MEDCouplingFieldDouble *getNodeK1() const; + MEDCoupling::MEDCouplingFieldDouble *getNodeK2() const; + MEDCoupling::MEDCouplingFieldDouble *getNodePrimitiveType() const; + MEDCoupling::MEDCouplingFieldDouble *getNodeNormal() const; + + // Area properties + MEDCoupling::MEDCouplingFieldDouble *getAreaId() const; + MEDCoupling::MEDCouplingFieldDouble *getAreaPrimitiveType() const; + MEDCoupling::MEDCouplingFieldDouble *getAreaNormal() const; + MEDCoupling::MEDCouplingFieldDouble *getMinorRadius() const; + MEDCoupling::MEDCouplingFieldDouble *getRadius() const; + MEDCoupling::MEDCouplingFieldDouble *getAngle() const; + MEDCoupling::MEDCouplingFieldDouble *getCenter() const; + MEDCoupling::MEDCouplingFieldDouble *getAxis() const; + MEDCoupling::MEDCouplingFieldDouble *getApex() const; + + protected: + ShapeRecognMesh(); + ~ShapeRecognMesh(); + + private: + MEDCoupling::MEDCouplingFieldDouble *nodeK1; + MEDCoupling::MEDCouplingFieldDouble *nodeK2; + MEDCoupling::MEDCouplingFieldDouble *nodePrimitiveType; + MEDCoupling::MEDCouplingFieldDouble *nodeNormal; + MEDCoupling::MEDCouplingFieldDouble *areaId; + MEDCoupling::MEDCouplingFieldDouble *areaPrimitiveType; + MEDCoupling::MEDCouplingFieldDouble *areaNormal; + MEDCoupling::MEDCouplingFieldDouble *minorRadius; + MEDCoupling::MEDCouplingFieldDouble *radius; + MEDCoupling::MEDCouplingFieldDouble *angle; + MEDCoupling::MEDCouplingFieldDouble *center; + MEDCoupling::MEDCouplingFieldDouble *axis; + MEDCoupling::MEDCouplingFieldDouble *apex; + }; +}; + +#endif // __SHAPERECOGNMESH_HXX__ diff --git a/src/ShapeRecogn/ShapeRecognMeshBuilder.cxx b/src/ShapeRecogn/ShapeRecognMeshBuilder.cxx new file mode 100644 index 000000000..f0a2b0581 --- /dev/null +++ b/src/ShapeRecogn/ShapeRecognMeshBuilder.cxx @@ -0,0 +1,250 @@ +#include "ShapeRecognMeshBuilder.hxx" + +#include "NodesBuilder.hxx" +#include "AreasBuilder.hxx" +#include "MEDLoader.hxx" +#include "ShapeRecognMesh.hxx" +#include "MEDCouplingFieldDouble.hxx" + +using namespace MEDCoupling; + +ShapeRecognMeshBuilder::ShapeRecognMeshBuilder(const std::string &fileName, int meshDimRelToMax) +{ + mesh = ReadUMeshFromFile(fileName, meshDimRelToMax); + if (mesh->getMeshDimension() != 2) + throw INTERP_KERNEL::Exception("Expect a mesh with a dimension equal to 2"); + if (mesh->getNumberOfCellsWithType(INTERP_KERNEL::NORM_TRI3) != mesh->getNumberOfCells()) + throw INTERP_KERNEL::Exception("Expect a mesh containing exclusively triangular cells"); +} + +ShapeRecognMeshBuilder::~ShapeRecognMeshBuilder() +{ + if (areas != nullptr) + delete areas; + if (nodes != nullptr) + delete nodes; + mesh->decrRef(); +} + +ShapeRecognMesh *ShapeRecognMeshBuilder::recognize() +{ + mesh->incrRef(); + NodesBuilder nodesBuilder(mesh); + nodes = nodesBuilder.build(); + AreasBuilder areasBuilder(nodes); + areasBuilder.build(); + areas = areasBuilder.getAreas(); + MCAuto recognMesh = ShapeRecognMesh::New(); + recognMesh->nodeK1 = buildNodeK1(); + recognMesh->nodeK2 = buildNodeK2(); + recognMesh->nodePrimitiveType = buildNodePrimitiveType(); + recognMesh->nodeNormal = buildNodeNormal(); + recognMesh->areaId = buildAreaId(); + recognMesh->areaPrimitiveType = buildAreaPrimitiveType(); + recognMesh->areaNormal = buildAreaNormal(); + recognMesh->minorRadius = buildMinorRadius(); + recognMesh->radius = buildRadius(); + recognMesh->angle = buildAngle(); + recognMesh->center = buildCenter(); + recognMesh->axis = buildAxis(); + recognMesh->apex = buildApex(); + return recognMesh.retn(); +} + +const Nodes *ShapeRecognMeshBuilder::getNodes() const +{ + return nodes; +} + +const Areas *ShapeRecognMeshBuilder::getAreas() const +{ + return areas; +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildNodeK1() const +{ + if (nodes == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + return buildField("K1 (Node)", 1, nodes->getK1()); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildNodeK2() const +{ + if (nodes == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + return buildField("K2 (Node)", 1, nodes->getK2()); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildNodePrimitiveType() const +{ + if (nodes == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + return buildField("Primitive Type (Node)", 1, nodes->getPrimitiveType()); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildNodeNormal() const +{ + if (nodes == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + return buildField("Normal (Node)", 3, nodes->getNormals()); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildAreaId() const +{ + if (areas == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + return buildField("Area Id", 1, areas->getAreaIdByNodes()); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildAreaPrimitiveType() const +{ + if (areas == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + double *values = buildAreaArray([](Areas *areas, mcIdType areaId) -> double + { return (double)areas->getPrimitiveType(areaId); }); + return buildField("Primitive Type (Area)", 1, values); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildAreaNormal() const +{ + if (areas == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + double *values = buildArea3DArray([](Areas *areas, mcIdType areaId) -> const std::array & + { return areas->getNormal(areaId); }); + return buildField("Normal (Area)", 3, values); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildMinorRadius() const +{ + if (areas == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + double *values = buildAreaArray([](Areas *areas, mcIdType areaId) -> double + { return areas->getMinorRadius(areaId); }); + return buildField("Minor Radius (Area)", 1, values); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildRadius() const +{ + if (areas == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + double *values = buildAreaArray([](Areas *areas, mcIdType areaId) -> double + { return areas->getRadius(areaId); }); + return buildField("Radius (Area)", 1, values); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildAngle() const +{ + if (areas == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + double *values = buildAreaArray([](Areas *areas, mcIdType areaId) -> double + { return areas->getAngle(areaId); }); + return buildField("Angle (Area)", 1, values); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildCenter() const +{ + if (areas == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + double *values = buildArea3DArray([](Areas *areas, mcIdType areaId) -> const std::array & + { return areas->getCenter(areaId); }); + return buildField("Center (Area)", 3, values); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildAxis() const +{ + if (areas == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + double *values = buildArea3DArray([](Areas *areas, mcIdType areaId) -> const std::array & + { return areas->getAxis(areaId); }); + return buildField("Axis (Area)", 3, values); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildApex() const +{ + if (areas == nullptr) + throw INTERP_KERNEL::Exception("recognize must be called before building any fields"); + double *values = buildArea3DArray([](Areas *areas, mcIdType areaId) -> const std::array & + { return areas->getApex(areaId); }); + return buildField("Apex (Area)", 3, values); +} + +template +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildField( + const std::string &name, + size_t nbOfCompo, + const std::vector &values) const +{ + DataArrayDouble *data = DataArrayDouble::New(); + data->setName(name); + data->alloc(nodes->getNbNodes(), nbOfCompo); + std::copy(values.begin(), values.end(), data->getPointer()); + data->declareAsNew(); + return buildField(name, nbOfCompo, data); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildField( + const std::string &name, + size_t nbOfCompo, + double *values) const +{ + DataArrayDouble *data = DataArrayDouble::New(); + data->setName(name); + data->useArray( + values, + true, + MEDCoupling::DeallocType::CPP_DEALLOC, + nodes->getNbNodes(), + nbOfCompo); + return buildField(name, nbOfCompo, data); +} + +MEDCouplingFieldDouble *ShapeRecognMeshBuilder::buildField( + const std::string &name, + size_t nbOfCompo, + DataArrayDouble *data) const +{ + MEDCouplingFieldDouble *field = MEDCouplingFieldDouble::New(ON_NODES); + field->setName(name); + field->setMesh(mesh); + if (nbOfCompo == 3) + { + data->setInfoOnComponent(0, "X"); + data->setInfoOnComponent(1, "Y"); + data->setInfoOnComponent(2, "Z"); + } + field->setArray(data); + data->decrRef(); + return field; +} + +double *ShapeRecognMeshBuilder::buildArea3DArray( + const std::array &(*areaFunc)(Areas *, mcIdType)) const +{ + double *values = new double[3 * nodes->getNbNodes()]; + const std::vector &areaIdByNodes = areas->getAreaIdByNodes(); + for (size_t nodeId = 0; nodeId < areaIdByNodes.size(); ++nodeId) + { + mcIdType areaId = areaIdByNodes[nodeId]; + if (areaId != -1) + { + const std::array &areaValues = areaFunc(areas, areaId); + values[3 * nodeId] = areaValues[0]; + values[3 * nodeId + 1] = areaValues[1]; + values[3 * nodeId + 2] = areaValues[2]; + } + } + return values; +} + +double *ShapeRecognMeshBuilder::buildAreaArray(double (*areaFunc)(Areas *, mcIdType)) const +{ + const std::vector &areaIdByNodes = areas->getAreaIdByNodes(); + double *values = new double[nodes->getNbNodes()]; + for (size_t nodeId = 0; nodeId < areaIdByNodes.size(); ++nodeId) + { + mcIdType areaId = areaIdByNodes[nodeId]; + if (areaId != -1) + values[nodeId] = areaFunc(areas, areaId); + } + return values; +} diff --git a/src/ShapeRecogn/ShapeRecognMeshBuilder.hxx b/src/ShapeRecogn/ShapeRecognMeshBuilder.hxx new file mode 100644 index 000000000..eee7c1507 --- /dev/null +++ b/src/ShapeRecogn/ShapeRecognMeshBuilder.hxx @@ -0,0 +1,85 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __SHAPERECOGNMESHBUILDER_HXX__ +#define __SHAPERECOGNMESHBUILDER_HXX__ + +#include + +#include "MEDCouplingUMesh.hxx" +#include "MEDCouplingFieldDouble.hxx" + +namespace MEDCoupling +{ + class Nodes; + class Areas; + class ShapeRecognMesh; + + class ShapeRecognMeshBuilder + { + public: + ShapeRecognMeshBuilder(const std::string &fileName, int meshDimRelToMax = 0); + ~ShapeRecognMeshBuilder(); + + const Nodes *getNodes() const; + const Areas *getAreas() const; + + ShapeRecognMesh *recognize(); + + private: + // Node properties + MEDCoupling::MEDCouplingFieldDouble *buildNodeK1() const; + MEDCoupling::MEDCouplingFieldDouble *buildNodeK2() const; + MEDCoupling::MEDCouplingFieldDouble *buildNodePrimitiveType() const; + MEDCoupling::MEDCouplingFieldDouble *buildNodeNormal() const; + + // Area properties + MEDCoupling::MEDCouplingFieldDouble *buildAreaId() const; + MEDCoupling::MEDCouplingFieldDouble *buildAreaPrimitiveType() const; + MEDCoupling::MEDCouplingFieldDouble *buildAreaNormal() const; + MEDCoupling::MEDCouplingFieldDouble *buildMinorRadius() const; + MEDCoupling::MEDCouplingFieldDouble *buildRadius() const; + MEDCoupling::MEDCouplingFieldDouble *buildAngle() const; + MEDCoupling::MEDCouplingFieldDouble *buildCenter() const; + MEDCoupling::MEDCouplingFieldDouble *buildAxis() const; + MEDCoupling::MEDCouplingFieldDouble *buildApex() const; + + template + MEDCouplingFieldDouble *buildField( + const std::string &name, + size_t nbOfCompo, + const std::vector &values) const; + MEDCouplingFieldDouble *buildField( + const std::string &name, + size_t nbOfCompo, + double *values) const; + MEDCouplingFieldDouble *buildField( + const std::string &name, + size_t nbOfCompo, + DataArrayDouble *values) const; + double *buildArea3DArray(const std::array &(*areaFunc)(Areas *, mcIdType)) const; + double *buildAreaArray(double (*areaFunc)(Areas *, mcIdType)) const; + + const MEDCouplingUMesh *mesh; + Nodes *nodes = nullptr; + Areas *areas = nullptr; + }; +}; + +#endif // __SHAPERECOGNMESHBUILDER_HXX__ diff --git a/src/ShapeRecogn/ShapeRecongConstants.hxx b/src/ShapeRecogn/ShapeRecongConstants.hxx new file mode 100644 index 000000000..801df4dfa --- /dev/null +++ b/src/ShapeRecogn/ShapeRecongConstants.hxx @@ -0,0 +1,43 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __SHAPERECOGNCONSTANTS_HXX__ +#define __SHAPERECOGNCONSTANTS_HXX__ + +namespace MEDCoupling +{ + // Nodes + constexpr double EPSILON_PRIMITIVE = 0.005; + // Areas + // - Match + constexpr double TOL_MATCH_CYLINDER = 0.05; + constexpr double TOL_MATCH_SPHERE = 0.05; + // - Relative distance + constexpr double DELTA_PLANE = 0.05; + constexpr double DELTA_SPHERE = 0.05; + constexpr double DELTA_CYLINDER = 0.05; + constexpr double DELTA_CONE = 0.05; + // - Invalid Zones + constexpr double THETA_MAX_CYLINDER = 0.05; + // - Thresholds + constexpr int THRESHOLD_MIN_NB_NODES = 5; + constexpr int THRESHOLD_MAX_NB_AREAS = 500; +}; + +#endif //__SHAPERECOGNCONSTANTS_HXX__ \ No newline at end of file diff --git a/src/ShapeRecogn/Swig/CMakeLists.txt b/src/ShapeRecogn/Swig/CMakeLists.txt new file mode 100644 index 000000000..8e839d156 --- /dev/null +++ b/src/ShapeRecogn/Swig/CMakeLists.txt @@ -0,0 +1,83 @@ +# Copyright (C) 2012-2024 CEA, EDF +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +FIND_PACKAGE(SWIG REQUIRED) +INCLUDE(${SWIG_USE_FILE}) + +ADD_DEFINITIONS(${PYTHON_DEFINITIONS}) + +SET_SOURCE_FILES_PROPERTIES(ShapeRecogn.i PROPERTIES CPLUSPLUS ON) +IF ("${PYTHON_VERSION_MAJOR}" STREQUAL "3") + SET_SOURCE_FILES_PROPERTIES(ShapeRecogn.i PROPERTIES SWIG_FLAGS "-py3") +ELSE() + SET_SOURCE_FILES_PROPERTIES(ShapeRecogn.i PROPERTIES SWIG_DEFINITIONS "-shadow") +ENDIF() + +SET(SWIG_MODULE_ShapeRecogn_EXTRA_FLAGS "") +IF(MEDCOUPLING_USE_64BIT_IDS) + STRING(APPEND SWIG_MODULE_ShapeRecogn_EXTRA_FLAGS "-DMEDCOUPLING_USE_64BIT_IDS") +ENDIF(MEDCOUPLING_USE_64BIT_IDS) + +SET (ShapeRecogn_SWIG_DPYS_FILES + ShapeRecogn.i +) + +INCLUDE_DIRECTORIES( + ${PYTHON_INCLUDE_DIRS} + ${MEDFILE_INCLUDE_DIRS} + ${HDF5_INCLUDE_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_SOURCE_DIR}/../../MEDLoader + ${CMAKE_CURRENT_SOURCE_DIR}/../../MEDCoupling_Swig + ${CMAKE_CURRENT_SOURCE_DIR}/../../MEDCoupling + ${CMAKE_CURRENT_SOURCE_DIR}/../../INTERP_KERNEL + ${CMAKE_CURRENT_SOURCE_DIR}/../../INTERP_KERNEL/Bases +) + +SET (SWIG_MODULE_ShapeRecogn_EXTRA_DEPS ${ShapeRecogn_SWIG_DPYS_FILES} + ${shaperecogn_HEADERS_HXX} + ${medcoupling_HEADERS_HXX} ${medcoupling_HEADERS_TXX} + ${interpkernel_HEADERS_HXX} ${interpkernel_HEADERS_TXX}) + +IF(WIN32) + SET_PROPERTY(SOURCE ShapeRecogn.i PROPERTY COMPILE_DEFINITIONS WIN32) +ENDIF() + +IF(${CMAKE_VERSION} VERSION_LESS "3.8.0") + SWIG_ADD_MODULE(ShapeRecogn python ShapeRecogn.i) +ELSE() + SWIG_ADD_LIBRARY(ShapeRecogn LANGUAGE python SOURCES ShapeRecogn.i) +ENDIF() + +SWIG_LINK_LIBRARIES(ShapeRecogn ${PYTHON_LIBRARIES} ${PLATFORM_LIBS} shaperecogn medloader medcouplingcpp) +SWIG_CHECK_GENERATION(ShapeRecogn) +IF(WIN32) + SET_TARGET_PROPERTIES(_ShapeRecogn PROPERTIES DEBUG_OUTPUT_NAME _ShapeRecogn_d) + # To increase the size of the .obj file on Windows because ShapeRecognPYTHON_wrap.cxx, generated by SWIG, is too big + TARGET_COMPILE_OPTIONS(_ShapeRecogn PRIVATE /bigobj) +ENDIF(WIN32) + +INSTALL(TARGETS _ShapeRecogn DESTINATION ${MEDCOUPLING_INSTALL_PYTHON}) +INSTALL(FILES ShapeRecogn.i DESTINATION ${MEDCOUPLING_INSTALL_HEADERS}) + +SALOME_INSTALL_SCRIPTS( + ${CMAKE_CURRENT_BINARY_DIR}/ShapeRecogn.py + ${MEDCOUPLING_INSTALL_PYTHON} + EXTRA_DPYS "${SWIG_MODULE_ShapeRecogn_REAL_NAME}") diff --git a/src/ShapeRecogn/Swig/ShapeRecogn.i b/src/ShapeRecogn/Swig/ShapeRecogn.i new file mode 100644 index 000000000..58893b0c6 --- /dev/null +++ b/src/ShapeRecogn/Swig/ShapeRecogn.i @@ -0,0 +1,15 @@ +%module ShapeRecogn + +%include "std_string.i" +%include "MEDCouplingCommon.i" + +%{ +#include "ShapeRecognMesh.hxx" +#include "ShapeRecognMeshBuilder.hxx" +using namespace MEDCoupling; +%} + +%ignore getAreas() const; +%ignore getNodes() const; +%include "ShapeRecognMesh.hxx" +%include "ShapeRecognMeshBuilder.hxx" diff --git a/src/ShapeRecogn/Test/CMakeLists.txt b/src/ShapeRecogn/Test/CMakeLists.txt new file mode 100644 index 000000000..2dc29d57c --- /dev/null +++ b/src/ShapeRecogn/Test/CMakeLists.txt @@ -0,0 +1,65 @@ +# Copyright (C) 2012-2024 CEA, EDF +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +INCLUDE_DIRECTORIES( + ${CPPUNIT_INCLUDE_DIRS} + ${HDF5_INCLUDE_DIRS} + ${MEDFILE_INCLUDE_DIRS} + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_SOURCE_DIR}/../../MEDLoader + ${CMAKE_CURRENT_SOURCE_DIR}/../../MEDCoupling + ${CMAKE_CURRENT_SOURCE_DIR}/../../INTERP_KERNEL + ${CMAKE_CURRENT_SOURCE_DIR}/../../INTERP_KERNEL/Bases + ${CMAKE_CURRENT_SOURCE_DIR}/../../INTERP_KERNELTest # For common CppUnitTest.hxx file and TestIKUtils.hxx + ) + +SET(TestShapeRecogn_SOURCES + TestShapeRecogn.cxx + MathOpsTest.cxx + PlaneTest.cxx + CylinderTest.cxx + ConeTest.cxx + SphereTest.cxx + TorusTest.cxx +) + +SALOME_ACCUMULATE_ENVIRONMENT(MEDCOUPLING_RESOURCE_DIR "${CMAKE_BINARY_DIR}/resources") +SALOME_GENERATE_TESTS_ENVIRONMENT(tests_env) + +ADD_EXECUTABLE(TestShapeRecogn ${TestShapeRecogn_SOURCES}) +TARGET_LINK_LIBRARIES(TestShapeRecogn shaperecogn InterpKernelTestUtils ${CPPUNIT_LIBRARIES} ${PLATFORM_LIBS}) + +INSTALL(TARGETS TestShapeRecogn DESTINATION ${MEDCOUPLING_INSTALL_BINS}) + +SET(BASE_TESTS TestShapeRecogn) + +FOREACH(test ${BASE_TESTS}) + ADD_TEST(NAME ${test} + COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/MCTestLauncher.py ${CMAKE_CURRENT_BINARY_DIR}/${test}) + SET_TESTS_PROPERTIES(${test} PROPERTIES ENVIRONMENT "${tests_env}") +ENDFOREACH() + +# Application tests + +SET(TEST_INSTALL_DIRECTORY ${MEDCOUPLING_INSTALL_TESTS}/ShapeRecogn) +INSTALL(TARGETS TestShapeRecogn DESTINATION ${TEST_INSTALL_DIRECTORY}) + +INSTALL(FILES CTestTestfileInstall.cmake + DESTINATION ${TEST_INSTALL_DIRECTORY} + RENAME CTestTestfile.cmake) diff --git a/src/ShapeRecogn/Test/CTestTestfileInstall.cmake b/src/ShapeRecogn/Test/CTestTestfileInstall.cmake new file mode 100644 index 000000000..ab32ede46 --- /dev/null +++ b/src/ShapeRecogn/Test/CTestTestfileInstall.cmake @@ -0,0 +1,31 @@ +# Copyright (C) 2015-2024 CEA, EDF +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +SET(TEST_NAMES + TestShapeRecogn +) + +FOREACH(tfile ${TEST_NAMES}) + SET(TEST_NAME ${COMPONENT_NAME}_${tfile}) + ADD_TEST(${TEST_NAME} python3 MCTestLauncher.py ${tfile}) + SET_TESTS_PROPERTIES(${TEST_NAME} PROPERTIES + LABELS "${COMPONENT_NAME}" + TIMEOUT ${TIMEOUT} + ) +ENDFOREACH() diff --git a/src/ShapeRecogn/Test/ConeTest.cxx b/src/ShapeRecogn/Test/ConeTest.cxx new file mode 100644 index 000000000..d25f4a792 --- /dev/null +++ b/src/ShapeRecogn/Test/ConeTest.cxx @@ -0,0 +1,324 @@ +#include "ConeTest.hxx" + +#include "ShapeRecognMeshBuilder.hxx" +#include "Areas.hxx" +#include "MathOps.hxx" +#include "TestInterpKernelUtils.hxx" // getResourceFile() + +using namespace MEDCoupling; + +void ConeTest::setUp() +{ + std::string file = INTERP_TEST::getResourceFile("ShapeRecognCone.med", 3); + srMesh = new ShapeRecognMeshBuilder(file); + srMesh->recognize(); + areas = srMesh->getAreas(); +} + +void ConeTest::tearDown() +{ + if (srMesh != 0) + delete srMesh; + areas = 0; +} + +void ConeTest::testNumberOfAreas() +{ + CPPUNIT_ASSERT_EQUAL(3, (int)areas->getNumberOfAreas()); +} + +void ConeTest::testComputePlaneProperties() +{ + const Nodes *nodes = srMesh->getNodes(); + Areas areas(nodes); + mcIdType areaId = areas.addArea(); + std::vector nodeIds{560, 561, 562, 563, 564}; + // Check the coordinates + std::vector coordsRef{ + -0.13274028, + -1.19035036, + 7.0, + 0.62579096, + -0.97231524, + 7.0, + -0.75942852, + -0.99273623, + 7.0, + 0.76454326, + 0.85588873, + 7.0, + 1.12546475, + -0.04924272, + 7.0, + }; + for (size_t i = 0; i < nodeIds.size(); ++i) + { + mcIdType nodeId = nodeIds[i]; + areas.addNode(areaId, nodeId); + std::array coords = nodes->getCoordinates(nodeId); + CPPUNIT_ASSERT_DOUBLES_EQUAL(coordsRef[3 * i], coords[0], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(coordsRef[3 * i + 1], coords[1], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(coordsRef[3 * i + 2], coords[2], 1E-6); + } + areas.computeProperties(areaId); + // Check normal + std::array normal = areas.getNormal(areaId); + std::array normalRef{0.0, 0.0, 1.0}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(normalRef[i], normal[i], 1E-6); +} + +void ConeTest::testComputeCylinderProperties() +{ + const Nodes *nodes = srMesh->getNodes(); + Areas areas(nodes); + mcIdType areaId = areas.addArea(); + // check coordinates + std::vector coordsRef{ + 8.58402821e+00, + -2.10248053e-15, + 1.41597179e+00, + 7.87604232e+00, + -1.92907400e-15, + 2.12395768e+00, + 7.16805642e+00, + -1.75566747e-15, + 2.83194358e+00, + }; + // Add nodes + std::vector nodeIds{26, 27, 28}; + for (size_t i = 0; i < nodeIds.size(); ++i) + { + mcIdType nodeId = nodeIds[i]; + areas.addNode(areaId, nodeId); + std::array coords = nodes->getCoordinates(nodeId); + CPPUNIT_ASSERT_DOUBLES_EQUAL(coordsRef[3 * i], coords[0], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(coordsRef[3 * i + 1], coords[1], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(coordsRef[3 * i + 2], coords[2], 1E-6); + } + areas.computeProperties(areaId); + // Check radius + double radius = areas.getRadius(areaId); + double radiusRef = 11.016809459292505; + CPPUNIT_ASSERT_DOUBLES_EQUAL(radiusRef, radius, 1E-6); + // Check axis + std::array axis = areas.getAxis(areaId); + std::array axisRef{-2.85185021e-02, 2.47000552e-05, 9.99593264e-01}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(axisRef[i], axis[i], 1E-6); + // Check axis point + std::array axisPoint = areas.getAxisPoint(areaId); + std::array axisPointRef{9.55163389e-02, -2.27195412e-05, -5.67562579e+00}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(axisPointRef[i], axisPoint[i], 1E-6); +} + +void ConeTest::testComputeConeProperties() +{ + std::srand(0); + const Nodes *nodes = srMesh->getNodes(); + Areas areas(nodes); + mcIdType areaId = areas.addArea(PrimitiveType::Cone); + std::vector coordsRef{ + 8.58402821e+00, + -2.10248053e-15, + 1.41597179e+00, + 7.87604232e+00, + -1.92907400e-15, + 2.12395768e+00, + 7.16805642e+00, + -1.75566747e-15, + 2.83194358e+00, + 6.46007053e+00, + -1.58226094e-15, + 3.53992947e+00}; + // Add nodes + std::vector nodeIds{26, 27, 28, 29}; + for (size_t i = 0; i < nodeIds.size(); ++i) + { + mcIdType nodeId = nodeIds[i]; + areas.addNode(areaId, nodeId); + std::array coords = nodes->getCoordinates(nodeId); + CPPUNIT_ASSERT_DOUBLES_EQUAL(coordsRef[3 * i], coords[0], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(coordsRef[3 * i + 1], coords[1], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(coordsRef[3 * i + 2], coords[2], 1E-6); + } + areas.computeProperties(areaId); + // Radius + double radiusRef = 10.546213172989718; + CPPUNIT_ASSERT_DOUBLES_EQUAL(radiusRef, areas.getRadius(areaId), 1E-6); + // Angle + double angleRef = 0.7567719849399294; + CPPUNIT_ASSERT_DOUBLES_EQUAL(angleRef, areas.getAngle(areaId), 1E-6); + // Axis + std::array axisRef{-2.99093478e-02, 1.48776702e-05, 9.99552615e-01}; + std::array axis = areas.getAxis(areaId); + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(axisRef[i], axis[i], 1E-6); + // Axis Point + std::array axisPointRef{ + 7.43242419e-02, -1.70396559e-05, -4.98890939e+00}; + std::array axisPoint = areas.getAxisPoint(areaId); + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(axisPointRef[i], axisPoint[i], 1E-6); + // Apex + std::array apexRef{ + -3.87870047e-01, 1.98282729e-04, 1.03928162e+01}; + std::array apex = areas.getApex(areaId); + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(apexRef[i], apex[i], 1E-6); +} + +void ConeTest::testFirstArea() +{ + // primitive type + CPPUNIT_ASSERT_EQUAL(PrimitiveType::Plane, areas->getPrimitiveType(0)); + // node ids + std::vector nodeIdsRef{ + 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, + 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, + 571}; + std::vector nodeIds = areas->getNodeIds(0); + std::sort(nodeIds.begin(), nodeIds.end()); + CPPUNIT_ASSERT_EQUAL(nodeIdsRef.size(), nodeIds.size()); + for (size_t i = 0; i < nodeIds.size(); ++i) + CPPUNIT_ASSERT_EQUAL(nodeIdsRef[i], nodeIds[i]); + // normal + std::array normal = areas->getNormal(0); + std::array normalRef{0.0, 0.0, 1.0}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(normalRef[i], normal[i], 1E-6); +} + +void ConeTest::testSecondArea() +{ + // primitive type + CPPUNIT_ASSERT_EQUAL(PrimitiveType::Plane, areas->getPrimitiveType(0)); + // node ids + std::vector nodeIdsRef = { + 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, + 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, + 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, + 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, + 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, + 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, + 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, + 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, + 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, + 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, + 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, + 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, + 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, + 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, + 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, + 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, + 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, + 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, + 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, + 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, + 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, + 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, + 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, + 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, + 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, + 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, + 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, + 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, + 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, + 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, + 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, + 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, + 956}; + std::vector nodeIds = areas->getNodeIds(1); + std::sort(nodeIds.begin(), nodeIds.end()); + CPPUNIT_ASSERT_EQUAL(nodeIdsRef.size(), nodeIds.size()); + for (size_t i = 0; i < nodeIds.size(); ++i) + CPPUNIT_ASSERT_EQUAL(nodeIdsRef[i], nodeIds[i]); + // normal + std::array normal = areas->getNormal(1); + std::array normalRef = {0.0, 0.0, -1.0}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(normalRef[i], normal[i], 1E-6); +} + +void ConeTest::testThirdArea() +{ + // primitive type + CPPUNIT_ASSERT_EQUAL(PrimitiveType::Cone, areas->getPrimitiveType(2)); + // node ids + std::vector nodeIdsRef{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, + 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, + 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, + 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, + 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, + 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, + 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, + 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, + 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, + 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, + 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, + 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, + 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, + 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, + 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, + 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, + 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, + 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, + 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, + 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, + 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, + 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, + 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, + 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, + 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, + 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, + 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, + 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, + 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, + 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, + 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, + 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, + 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, + 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, + 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, + 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, + 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, + 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, + 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, + 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, + 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, + 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, + 540, 541, 542, 543, 544, 545, 546, 547, 548}; + std::vector nodeIds = areas->getNodeIds(2); + std::sort(nodeIds.begin(), nodeIds.end()); + CPPUNIT_ASSERT_EQUAL(nodeIdsRef.size(), nodeIds.size()); + for (size_t i = 0; i < nodeIds.size(); ++i) + CPPUNIT_ASSERT_EQUAL(nodeIdsRef[i], nodeIds[i]); + // radius + CPPUNIT_ASSERT_DOUBLES_EQUAL(10.0, areas->getRadius(2), 5E-1); + // angle + CPPUNIT_ASSERT_DOUBLES_EQUAL(M_PI_4, areas->getAngle(2), 1E-2); + // axis + std::array axis = areas->getAxis(2); + std::array axisRef{0.0, 0.0, 1.0}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(axisRef[i], axis[i], 1E-2); + // // axis point + // std::array axisPoint = areas->getAxisPoint(2); + // std::array axisPointRef{ + // -6.50039462e-03, -5.76665576e-05, -3.99198765e+00}; + // for (size_t i = 0; i < 3; ++i) + // CPPUNIT_ASSERT_DOUBLES_EQUAL( + // axisPointRef[i], axisPoint[i], 1E-2); + // apex + std::array apex = areas->getApex(2); + std::array apexRef{0.0, 0.0, 10.0}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(apexRef[i], apex[i], 1E-1); +} diff --git a/src/ShapeRecogn/Test/ConeTest.hxx b/src/ShapeRecogn/Test/ConeTest.hxx new file mode 100644 index 000000000..20b1bef3f --- /dev/null +++ b/src/ShapeRecogn/Test/ConeTest.hxx @@ -0,0 +1,62 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __CONETEST_HXX__ +#define __CONETEST_HXX__ + +#include +#include + +namespace MEDCoupling +{ + class ShapeRecognMeshBuilder; + class Areas; + + class ConeTest : public CppUnit::TestFixture + { + CPPUNIT_TEST_SUITE(ConeTest); + CPPUNIT_TEST(testNumberOfAreas); + CPPUNIT_TEST(testComputePlaneProperties); + CPPUNIT_TEST(testComputeCylinderProperties); + CPPUNIT_TEST(testComputeConeProperties); + CPPUNIT_TEST(testFirstArea); + CPPUNIT_TEST(testSecondArea); + CPPUNIT_TEST(testThirdArea); + CPPUNIT_TEST_SUITE_END(); + + public: + void setUp() override; + void tearDown() override; + + void testComputePlaneProperties(); + void testComputeCylinderProperties(); + void testComputeConeProperties(); + + void testNumberOfAreas(); + void testFirstArea(); + void testSecondArea(); + void testThirdArea(); + + private: + ShapeRecognMeshBuilder *srMesh = 0; + const Areas *areas; + }; +}; + +#endif // __CONETEST_HXX__ diff --git a/src/ShapeRecogn/Test/CylinderTest.cxx b/src/ShapeRecogn/Test/CylinderTest.cxx new file mode 100644 index 000000000..47f6ac3d3 --- /dev/null +++ b/src/ShapeRecogn/Test/CylinderTest.cxx @@ -0,0 +1,136 @@ +#include "CylinderTest.hxx" + +#include "ShapeRecognMeshBuilder.hxx" +#include "Areas.hxx" +#include "MathOps.hxx" +#include "TestInterpKernelUtils.hxx" // getResourceFile() + +using namespace MEDCoupling; + +void CylinderTest::setUp() +{ + std::string file = INTERP_TEST::getResourceFile("ShapeRecognCylinder.med", 3); + srMesh = new ShapeRecognMeshBuilder(file); + srMesh->recognize(); + areas = srMesh->getAreas(); +} + +void CylinderTest::tearDown() +{ + if (srMesh != 0) + delete srMesh; + areas = 0; +} + +void CylinderTest::testNumberOfAreas() +{ + CPPUNIT_ASSERT_EQUAL(3, (int)areas->getNumberOfAreas()); +} + +void CylinderTest::testFirstArea() +{ + // primitive type + CPPUNIT_ASSERT_EQUAL(PrimitiveType::Cylinder, areas->getPrimitiveType(0)); + // node ids + std::vector nodeIdsRef{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, + 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, + 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, + 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, + 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, + 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, + 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, + 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, + 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, + 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, + 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, + 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, + 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, + 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, + 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, + 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, + 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, + 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, + 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, + 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, + 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, + 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, + 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, + 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, + 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, + 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, + 369}; + std::vector nodeIds = areas->getNodeIds(0); + std::sort(nodeIds.begin(), nodeIds.end()); + CPPUNIT_ASSERT_EQUAL(nodeIdsRef.size(), nodeIds.size()); + for (size_t i = 0; i < nodeIds.size(); ++i) + CPPUNIT_ASSERT_EQUAL(nodeIdsRef[i], nodeIds[i]); + // radius + CPPUNIT_ASSERT_DOUBLES_EQUAL(4.993494657779537, areas->getRadius(0), 1E-2); + // axis + std::array axis = areas->getAxis(0); + std::array axisRef{7.66631075e-04, -1.59966800e-04, 9.99999693e-01}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(axisRef[i], axis[i], 1E-4); + // axis point + std::array axisPoint = areas->getAxisPoint(0); + std::array axisPointRef{3.78927537e-03, -2.03409415e-03, 5.03355802e+00}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL( + axisPointRef[i], axisPoint[i], 1E-2); +} + +void CylinderTest::testSecondArea() +{ + // primitive type + CPPUNIT_ASSERT_EQUAL(PrimitiveType::Plane, areas->getPrimitiveType(1)); + // node ids + std::vector nodeIdsRef{ + 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, + 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, + 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, + 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, + 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, + 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, + 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, + 440, 441, 442}; + std::vector nodeIds = areas->getNodeIds(1); + std::sort(nodeIds.begin(), nodeIds.end()); + CPPUNIT_ASSERT_EQUAL(nodeIdsRef.size(), nodeIds.size()); + for (size_t i = 0; i < nodeIds.size(); ++i) + CPPUNIT_ASSERT_EQUAL(nodeIdsRef[i], nodeIds[i]); + // normal + std::array normal = areas->getNormal(1); + std::array normalRef{0.0, 0.0, 1.0}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(normalRef[i], normal[i], 1E-6); +} + +void CylinderTest::testThirdArea() +{ + // primitive type + CPPUNIT_ASSERT_EQUAL(PrimitiveType::Plane, areas->getPrimitiveType(2)); + // node ids + std::vector nodeIdsRef{ + 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, + 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, + 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, + 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, + 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, + 493, 494, 495, 496, 497, 498, 499}; + std::vector nodeIds = areas->getNodeIds(2); + std::sort(nodeIds.begin(), nodeIds.end()); + CPPUNIT_ASSERT_EQUAL(nodeIdsRef.size(), nodeIds.size()); + for (size_t i = 0; i < nodeIds.size(); ++i) + CPPUNIT_ASSERT_EQUAL(nodeIdsRef[i], nodeIds[i]); + // normal + std::array normal = areas->getNormal(2); + std::array normalRef{0.0, 0.0, -1.0}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(normalRef[i], normal[i], 1E-6); +} diff --git a/src/ShapeRecogn/Test/CylinderTest.hxx b/src/ShapeRecogn/Test/CylinderTest.hxx new file mode 100644 index 000000000..a35ac1481 --- /dev/null +++ b/src/ShapeRecogn/Test/CylinderTest.hxx @@ -0,0 +1,55 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __CYLINDERTEST_HXX__ +#define __CYLINDERTEST_HXX__ + +#include +#include + +namespace MEDCoupling +{ + class ShapeRecognMeshBuilder; + class Areas; + + class CylinderTest : public CppUnit::TestFixture + { + CPPUNIT_TEST_SUITE(CylinderTest); + CPPUNIT_TEST(testNumberOfAreas); + CPPUNIT_TEST(testFirstArea); + CPPUNIT_TEST(testSecondArea); + CPPUNIT_TEST(testThirdArea); + CPPUNIT_TEST_SUITE_END(); + + public: + void setUp() override; + void tearDown() override; + + void testNumberOfAreas(); + void testFirstArea(); + void testSecondArea(); + void testThirdArea(); + + private: + ShapeRecognMeshBuilder *srMesh = 0; + const Areas *areas; + }; +}; + +#endif // __CYLINDERTEST_HXX__ diff --git a/src/ShapeRecogn/Test/MathOpsTest.cxx b/src/ShapeRecogn/Test/MathOpsTest.cxx new file mode 100644 index 000000000..033f9921b --- /dev/null +++ b/src/ShapeRecogn/Test/MathOpsTest.cxx @@ -0,0 +1,97 @@ +#include "MathOpsTest.hxx" +#include "MathOps.hxx" + +using namespace MEDCoupling; + +void MathOpsTest::testLstsq() +{ + std::vector a = { + 0.69473263, 0.83318004, 0.60822673, 0.59243878, 0.82872553, + 0.84048546, 0.95698819, 0.02171218, 0.27683381, 0.20628928, + 0.80895323, 0.4207767, 0.37468575, 0.86258204, 0.42571846}; + std::vector b = { + 0.91167508, 0.95878824, 0.7234827, 0.51753917, 0.18603306}; + std::vector x = MathOps::lstsq(a, b); + std::array xRef = {0.35719095, 0.5134345, 0.26039343}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(xRef[i], x[i], 1E-6); +} + +void MathOpsTest::testLstsq2() +{ + std::vector a = { + 0.4564562, 0.3517006, + 0.28928215, 0.72309086, + 0.05944836, 0.56024464}; + std::vector b = {0.98902712, 0.46791812}; + std::vector x = MathOps::lstsq(a, b); + std::array xRef = {2.10752524, 0.2636243, -0.82807416}; + for (size_t i = 0; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(xRef[i], x[i], 1E-6); +} + +void MathOpsTest::testLstsqBig() +{ + std::vector a(5000000 * 3, 1.0); + std::vector b(5000000, 1.0); + std::vector x = MathOps::lstsq(a, b); + std::array xRef = {1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0}; + for (size_t i = 2; i < 3; ++i) + CPPUNIT_ASSERT_DOUBLES_EQUAL(xRef[i], x[i], 1E-6); +} + +void MathOpsTest::testComputeCov() +{ + std::vector coordinates{ + 1, 2, -3, -8, 6, -3, 9, 2, -1, -10, -11, -120}; + std::array + covMatrix = MathOps::computeCov(coordinates); + CPPUNIT_ASSERT_DOUBLES_EQUAL(76.666666, covMatrix[0], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(26.666666, covMatrix[1], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(319.333333, covMatrix[2], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(26.666666, covMatrix[3], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(54.916666, covMatrix[4], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(420.75, covMatrix[5], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(319.333333, covMatrix[6], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(420.75, covMatrix[7], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(3462.25, covMatrix[8], 1E-6); +} + +void MathOpsTest::testComputePCAFirstAxis() +{ + std::vector coordinates{ + 1, 2, -3, -8, 6, -3, 9, 2, -1, -10, -11, -120}; + std::array + axis = MathOps::computePCAFirstAxis(coordinates); + CPPUNIT_ASSERT_DOUBLES_EQUAL(0.09198798, axis[0], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(0.11994164, axis[1], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(0.9885101, axis[2], 1E-6); +} + +void MathOpsTest::testComputeAngles() +{ + std::vector directions{ + 1, 2, -3, -8, 6, -3, 9, 2, -1, -10, -11, -120}; + std::array axis{1, 2, 3}; + std::vector angles = MathOps::computeAngles( + directions, axis); + CPPUNIT_ASSERT_DOUBLES_EQUAL(1.86054803, angles[0], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(1.69914333, angles[1], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(1.27845478, angles[2], 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(2.61880376, angles[3], 1E-6); +} + +void MathOpsTest::testComputeBaseFromNormal() +{ + std::array normal = {1.0, 2.0, 3.0}; + std::array base = MathOps::computeBaseFromNormal(normal); + std::array baseRef = { + -0.53452248, 0.77454192, -0.33818712, + -0.80178373, -0.33818712, 0.49271932}; + for (size_t i = 0; i < 6; ++i) + { + std::ostringstream message; + message << "Mismatch at index " << i; + CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(message.str().c_str(), baseRef[i], base[i], 1E-6); + } +} diff --git a/src/ShapeRecogn/Test/MathOpsTest.hxx b/src/ShapeRecogn/Test/MathOpsTest.hxx new file mode 100644 index 000000000..2988a950b --- /dev/null +++ b/src/ShapeRecogn/Test/MathOpsTest.hxx @@ -0,0 +1,51 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __MATHOPSTEST_HXX__ +#define __MATHOPSTEST_HXX__ + +#include +#include + +namespace MEDCoupling +{ + class MathOpsTest : public CppUnit::TestFixture + { + CPPUNIT_TEST_SUITE(MathOpsTest); + CPPUNIT_TEST(testLstsq); + CPPUNIT_TEST(testLstsq2); + CPPUNIT_TEST(testLstsqBig); + CPPUNIT_TEST(testComputeCov); + CPPUNIT_TEST(testComputePCAFirstAxis); + CPPUNIT_TEST(testComputeAngles); + CPPUNIT_TEST(testComputeBaseFromNormal); + CPPUNIT_TEST_SUITE_END(); + + public: + static void testLstsq(); + static void testLstsq2(); + static void testLstsqBig(); + static void testComputeCov(); + static void testComputePCAFirstAxis(); + static void testComputeAngles(); + static void testComputeBaseFromNormal(); + }; +}; + +#endif // __MATHOPSTEST_HXX__ diff --git a/src/ShapeRecogn/Test/PlaneTest.cxx b/src/ShapeRecogn/Test/PlaneTest.cxx new file mode 100644 index 000000000..6f47f161c --- /dev/null +++ b/src/ShapeRecogn/Test/PlaneTest.cxx @@ -0,0 +1,44 @@ +#include "PlaneTest.hxx" + +#include "ShapeRecognMeshBuilder.hxx" +#include "Areas.hxx" +#include "MathOps.hxx" +#include "TestInterpKernelUtils.hxx" // getResourceFile() + +using namespace MEDCoupling; + +void PlaneTest::setUp() +{ + std::string file = INTERP_TEST::getResourceFile("ShapeRecognPlane.med", 3); + srMesh = new ShapeRecognMeshBuilder(file); + srMesh->recognize(); + areas = srMesh->getAreas(); +} + +void PlaneTest::tearDown() +{ + if (srMesh != 0) + delete srMesh; + areas = 0; +} + +void PlaneTest::testArea() +{ + CPPUNIT_ASSERT_EQUAL(36, (int)areas->getNumberOfNodes(0)); + CPPUNIT_ASSERT_EQUAL(1, (int)areas->getNumberOfAreas()); + // Normal + std::array normal = areas->getNormal(0); + std::array normalRef = {0.781525, 0.310606, -0.541056}; + std::array affinePoint = areas->getAffinePoint(0); + double proportion0 = normal[0] / normalRef[0]; + double proportion1 = normal[1] / normalRef[1]; + double proportion2 = normal[2] / normalRef[2]; + double proportion3 = MathOps::dot(normal, affinePoint) / MathOps::dot(normalRef, affinePoint); + // Check proportions between the normal vectors of the two planes + CPPUNIT_ASSERT_DOUBLES_EQUAL(proportion0, proportion1, 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(proportion1, proportion2, 1E-6); + CPPUNIT_ASSERT_DOUBLES_EQUAL(proportion2, proportion3, 1E-6); + // Check the angle + double angle = MathOps::computeAngle(normal, normalRef); + CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, angle, 1E-6); +} \ No newline at end of file diff --git a/src/ShapeRecogn/Test/PlaneTest.hxx b/src/ShapeRecogn/Test/PlaneTest.hxx new file mode 100644 index 000000000..af6eb4755 --- /dev/null +++ b/src/ShapeRecogn/Test/PlaneTest.hxx @@ -0,0 +1,49 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __PLANETEST_HXX__ +#define __PLANETEST_HXX__ + +#include +#include + +namespace MEDCoupling +{ + class ShapeRecognMeshBuilder; + class Areas; + + class PlaneTest : public CppUnit::TestFixture + { + CPPUNIT_TEST_SUITE(PlaneTest); + CPPUNIT_TEST(testArea); + CPPUNIT_TEST_SUITE_END(); + + public: + void setUp() override; + void tearDown() override; + + void testArea(); + + private: + ShapeRecognMeshBuilder *srMesh = 0; + const Areas *areas; + }; +}; + +#endif // __PLANETEST_HXX__ diff --git a/src/ShapeRecogn/Test/SphereTest.cxx b/src/ShapeRecogn/Test/SphereTest.cxx new file mode 100644 index 000000000..4d7f64ecb --- /dev/null +++ b/src/ShapeRecogn/Test/SphereTest.cxx @@ -0,0 +1,36 @@ +#include "SphereTest.hxx" + +#include "ShapeRecognMeshBuilder.hxx" +#include "Areas.hxx" +#include "MathOps.hxx" +#include "TestInterpKernelUtils.hxx" // getResourceFile() + +using namespace MEDCoupling; + +void SphereTest::setUp() +{ + std::string file = INTERP_TEST::getResourceFile("ShapeRecognSphere.med", 3); + srMesh = new ShapeRecognMeshBuilder(file); + srMesh->recognize(); + areas = srMesh->getAreas(); +} + +void SphereTest::tearDown() +{ + if (srMesh != 0) + delete srMesh; + areas = 0; +} + +void SphereTest::testArea() +{ + CPPUNIT_ASSERT_EQUAL(1, (int)areas->getNumberOfAreas()); + // 8 double nodes so 147 - 6 nodes + CPPUNIT_ASSERT_EQUAL(141, (int)areas->getNumberOfNodes(0)); + CPPUNIT_ASSERT_EQUAL(PrimitiveType::Sphere, areas->getPrimitiveType(0)); + CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, areas->getRadius(0), 1E-2); + std::array centerRef = {5.3, -6.7, -9.02}; + std::array center = areas->getCenter(0); + for (size_t j = 0; j < 3; ++j) + CPPUNIT_ASSERT_DOUBLES_EQUAL(centerRef[j], center[j], 1E-2); +} diff --git a/src/ShapeRecogn/Test/SphereTest.hxx b/src/ShapeRecogn/Test/SphereTest.hxx new file mode 100644 index 000000000..6500def00 --- /dev/null +++ b/src/ShapeRecogn/Test/SphereTest.hxx @@ -0,0 +1,49 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __SPHERETEST_HXX__ +#define __SPHERETEST_HXX__ + +#include +#include + +namespace MEDCoupling +{ + class ShapeRecognMeshBuilder; + class Areas; + + class SphereTest : public CppUnit::TestFixture + { + CPPUNIT_TEST_SUITE(SphereTest); + CPPUNIT_TEST(testArea); + CPPUNIT_TEST_SUITE_END(); + + public: + void setUp() override; + void tearDown() override; + + void testArea(); + + private: + ShapeRecognMeshBuilder *srMesh = 0; + const Areas *areas; + }; +}; + +#endif // __SPHERETEST_HXX__ diff --git a/src/ShapeRecogn/Test/TestShapeRecogn.cxx b/src/ShapeRecogn/Test/TestShapeRecogn.cxx new file mode 100644 index 000000000..b7cd2af14 --- /dev/null +++ b/src/ShapeRecogn/Test/TestShapeRecogn.cxx @@ -0,0 +1,34 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#include "MathOpsTest.hxx" +#include "PlaneTest.hxx" +#include "CylinderTest.hxx" +#include "ConeTest.hxx" +#include "SphereTest.hxx" +#include "TorusTest.hxx" + +CPPUNIT_TEST_SUITE_REGISTRATION(MEDCoupling::MathOpsTest); +CPPUNIT_TEST_SUITE_REGISTRATION(MEDCoupling::PlaneTest); +CPPUNIT_TEST_SUITE_REGISTRATION(MEDCoupling::CylinderTest); +CPPUNIT_TEST_SUITE_REGISTRATION(MEDCoupling::ConeTest); +CPPUNIT_TEST_SUITE_REGISTRATION(MEDCoupling::SphereTest); +CPPUNIT_TEST_SUITE_REGISTRATION(MEDCoupling::TorusTest); + +#include "BasicMainTest.hxx" diff --git a/src/ShapeRecogn/Test/TorusTest.cxx b/src/ShapeRecogn/Test/TorusTest.cxx new file mode 100644 index 000000000..6e24d10ee --- /dev/null +++ b/src/ShapeRecogn/Test/TorusTest.cxx @@ -0,0 +1,38 @@ +#include "TorusTest.hxx" + +#include "ShapeRecognMeshBuilder.hxx" +#include "Areas.hxx" +#include "MathOps.hxx" +#include "TestInterpKernelUtils.hxx" // getResourceFile() + +using namespace MEDCoupling; + +void TorusTest::setUp() +{ + std::string file = INTERP_TEST::getResourceFile("ShapeRecognTorus.med", 3); + srMesh = new ShapeRecognMeshBuilder(file); + srMesh->recognize(); + areas = srMesh->getAreas(); +} + +void TorusTest::tearDown() +{ + if (srMesh != 0) + delete srMesh; + areas = 0; +} + +void TorusTest::testArea() +{ + CPPUNIT_ASSERT_EQUAL(275, (int)srMesh->getNodes()->getNbNodes()); + CPPUNIT_ASSERT_EQUAL(1, (int)areas->getNumberOfAreas()); + CPPUNIT_ASSERT_EQUAL(PrimitiveType::Torus, areas->getPrimitiveType(0)); + // Some nodes are unknown + CPPUNIT_ASSERT_EQUAL(272, (int)areas->getNumberOfNodes(0)); + CPPUNIT_ASSERT_DOUBLES_EQUAL(0.843297, areas->getMinorRadius(0), 1E-2); + CPPUNIT_ASSERT_DOUBLES_EQUAL(1.156428, areas->getRadius(0), 1E-1); + std::array centerRef = {7.687022, -3.726887, -9.02}; + std::array center = areas->getCenter(0); + for (size_t j = 0; j < 3; ++j) + CPPUNIT_ASSERT_DOUBLES_EQUAL(centerRef[j], center[j], 1E-2); +} diff --git a/src/ShapeRecogn/Test/TorusTest.hxx b/src/ShapeRecogn/Test/TorusTest.hxx new file mode 100644 index 000000000..07ae86d3a --- /dev/null +++ b/src/ShapeRecogn/Test/TorusTest.hxx @@ -0,0 +1,49 @@ +// Copyright (C) 2007-2024 CEA, EDF +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +// + +#ifndef __TORUSTEST_HXX__ +#define __TORUSTEST_HXX__ + +#include +#include + +namespace MEDCoupling +{ + class ShapeRecognMeshBuilder; + class Areas; + + class TorusTest : public CppUnit::TestFixture + { + CPPUNIT_TEST_SUITE(TorusTest); + CPPUNIT_TEST(testArea); + CPPUNIT_TEST_SUITE_END(); + + public: + void setUp() override; + void tearDown() override; + + void testArea(); + + private: + ShapeRecognMeshBuilder *srMesh = 0; + const Areas *areas; + }; +}; + +#endif // __TORUSTEST_HXX__ -- 2.39.2