From 84ff112176a66f1ebae713d369356d5e5eddede8 Mon Sep 17 00:00:00 2001 From: hesuicong Date: Tue, 9 Dec 2025 10:56:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/compute_print_net.cpython-312.pyc | Bin 0 -> 30597 bytes __pycache__/config.cpython-312.pyc | Bin 0 -> 798 bytes __pycache__/download_print.cpython-312.pyc | Bin 0 -> 62934 bytes __pycache__/general.cpython-312.pyc | Bin 0 -> 12301 bytes ...est_position_of_center_ext.cpython-312.pyc | Bin 0 -> 23997 bytes ...t_lowest_position_of_z_out.cpython-312.pyc | Bin 0 -> 15344 bytes __pycache__/grid_near_three.cpython-312.pyc | Bin 0 -> 4611 bytes .../point_cloud_layout.cpython-312.pyc | Bin 0 -> 48442 bytes ...ctory_type_setting_obj_run.cpython-312.pyc | Bin 0 -> 9946 bytes .../print_merged_many_obj.cpython-312.pyc | Bin 0 -> 8833 bytes ...mplot3d_point_cloud_layout.cpython-312.pyc | Bin 0 -> 59514 bytes .../print_show_weight_max_obj.cpython-312.pyc | Bin 0 -> 32340 bytes __pycache__/test_load_json.cpython-312.pyc | Bin 0 -> 16905 bytes clound_print.py | 22 +- compute_print_net.py | 748 ++++ config.py | 18 + download_print.py | 70 +- download_print_out.py | 116 +- general.py | 286 ++ get_lowest_position_of_center_ext.py | 847 ---- get_lowest_position_of_z_out.py | 352 -- grid_near_three.py | 179 - output.log | 295 ++ point_cloud_layout.py | 1567 +++++++ print_factory_type_setting_obj_run.py | 174 +- print_factory_type_setting_obj_run_GUI.py | 193 - print_merged_many_obj.py | 162 - print_mplot3d_point_cloud_layout.py | 3619 ----------------- print_setting_run.py | 127 - print_setting_ui.py | 51 - print_show_weight_max_obj.py | 314 +- print_type_setting_gui.py | 346 -- print_type_setting_gui.spec | 38 - print_type_setting_gui_multi.py | 126 - qt5_demo.py | 25 - qt5_demo.spec | 44 - sui_01.py | 275 -- test.py | 19 - test_load_json.py | 34 +- x_y_min_test.py | 52 - 读写时间测试.py | 152 - 读写时间测试2.py | 98 - 42 files changed, 3032 insertions(+), 7317 deletions(-) create mode 100644 __pycache__/compute_print_net.cpython-312.pyc create mode 100644 __pycache__/config.cpython-312.pyc create mode 100644 __pycache__/download_print.cpython-312.pyc create mode 100644 __pycache__/general.cpython-312.pyc create mode 100644 __pycache__/get_lowest_position_of_center_ext.cpython-312.pyc create mode 100644 __pycache__/get_lowest_position_of_z_out.cpython-312.pyc create mode 100644 __pycache__/grid_near_three.cpython-312.pyc create mode 100644 __pycache__/point_cloud_layout.cpython-312.pyc create mode 100644 __pycache__/print_factory_type_setting_obj_run.cpython-312.pyc create mode 100644 __pycache__/print_merged_many_obj.cpython-312.pyc create mode 100644 __pycache__/print_mplot3d_point_cloud_layout.cpython-312.pyc create mode 100644 __pycache__/print_show_weight_max_obj.cpython-312.pyc create mode 100644 __pycache__/test_load_json.cpython-312.pyc create mode 100644 compute_print_net.py create mode 100644 config.py create mode 100644 general.py delete mode 100644 get_lowest_position_of_center_ext.py delete mode 100644 get_lowest_position_of_z_out.py delete mode 100644 grid_near_three.py create mode 100644 output.log create mode 100644 point_cloud_layout.py delete mode 100644 print_factory_type_setting_obj_run_GUI.py delete mode 100644 print_merged_many_obj.py delete mode 100644 print_mplot3d_point_cloud_layout.py delete mode 100644 print_setting_run.py delete mode 100644 print_setting_ui.py delete mode 100644 print_type_setting_gui.py delete mode 100644 print_type_setting_gui.spec delete mode 100644 print_type_setting_gui_multi.py delete mode 100644 qt5_demo.py delete mode 100644 qt5_demo.spec delete mode 100644 sui_01.py delete mode 100644 test.py delete mode 100644 x_y_min_test.py delete mode 100644 读写时间测试.py delete mode 100644 读写时间测试2.py diff --git a/__pycache__/compute_print_net.cpython-312.pyc b/__pycache__/compute_print_net.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93005921c902e37824acc95791c19aedd1dc59be GIT binary patch literal 30597 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 Vabw$aUBR7G=Bp+dQHY`9{{klMsu=(P literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73b2da645d1ecaab4a7f0b1cc0b7ea565171762d GIT binary patch literal 798 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 F@jrET@)H06 literal 0 HcmV?d00001 diff --git a/__pycache__/general.cpython-312.pyc b/__pycache__/general.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0135f55a00d53636e39a4a223c424d502245aebc GIT binary patch literal 12301 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 literal 0 HcmV?d00001 diff --git a/__pycache__/get_lowest_position_of_center_ext.cpython-312.pyc b/__pycache__/get_lowest_position_of_center_ext.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..caec8fb8d61019ce6f9af31288bb577078f34b7f GIT binary patch literal 23997 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%0A;f} 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 KI9|h*`Tqd@;`6is literal 0 HcmV?d00001 diff --git a/__pycache__/get_lowest_position_of_z_out.cpython-312.pyc b/__pycache__/get_lowest_position_of_z_out.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26e4818f648362ea3239b5098d41f20d87d5a7be GIT binary patch literal 15344 zcmeG@X>1$UnL~0&4lhw6saw`fVul;0 zaS*Cb)>PfD=wu5sLDA5OkvKqhs~*{=i(*@J|IB2x6?dv1UaTC)SS;!$1$MD0(EZ*F zhaAcDl^DSm*n#xs%{#vL-S55cJ^r)BV#MI-+tB7I*^FWTjy~i^m(G0gC&8z08Sw{}=7zIDSo14gRPV#9h;11Uk`kQzt}sXeP2Hj+B{GLd3bfPF7jaN%&#jA};ZwBAI8hep$NVha0^uvEcbvd zo=`w;$l@sH7$d2kBo2Nq=~^mFLAlAQl=nD7Qnwt-JPQ3?Z=0 zuyqw}jTt)4q>}19#wyc8S`jU%uUsO(!y1u3je@>>M)L-igSv;L?|0`-IG-#C7X&jj zF$GM123OmC`Xzgop9;z6@=*6{U7&e%0iz4+6@0QG`IGkVYZMxg^|B8<8rV!rMv|FX zBJ3awpMqH+To^727YEzq7NGa)D@8JAo`ShaY7uq>JLEPKopLVwW->k~T*8!0^ki$~ zoPuK;^_Rv>;rs8wvtoIyRhX6E0kd+sHF%$_?t!hZeH1f%1yjmYz*V z)Z3tW@DaJsJ?8Co_dQZ2v(AJumFcn6>2dBC&njjeQ?N612y2^v2xh0qM!9&W_h_g$snX!y4yiz95^kXFID+b*E(>+n!U{pOBzTNA=x_Pt0 zb8TU^$o8dx{MD@KFK6wv6+F<^H6Cbdh98Zx1u%`w26@g6_cCgxcT9a0bFMpd6Sf}M zP1uuTGz$8Ks*by9pT{@eTYZoUoUA_S4pjU6)u$;s=ozH~z15dBVmmRpi9$e^RRZol z;|YvE?|ARge+!-6F%E_B+}aV^nt?S;(E!Bnot~tsNh(N<1}U<&dO83DRlBFAX#W|{ zq#MfQHX%*>rm>K=ddyE(r{UG_OlPj`7=P_Q|1R-g|8%u)yz1ruIJ{8#FFQh|N2pP^ zcXZmDhH{4Tx&odM)m!cP42TOgWl(uS9Mn&H#yvi_w|dI&@dc|#z5Z!Z94%B=4J4EP z=X?S8Cbo<7=lq(RVrhrj;U_+K$GUqa- zHxa|3lWvsq1t~gI5OPT@a!CbU+e7x&>NSPk+nsfS#@|B9l{z)oG&j`kEAXe-~oDlTW zL66rHoDuSlfvo&=4=KF`U77(V#37f+B1*7NxuK_P+UEgzO}a+?UOz3Y8>fOUq!=!@ z*E8;;NY{ve+D8IIA#+C1Nu&yzNskW__ZdMu=JmUSLcx@qc6+@bKiVI32R(kDfRFm8 z0DqFAro>zw0xj3NKyhf#8P^!?pG=R3WFTlIs`a#+1nvX`o!9}89raI6O$R9#Vpf{d zf+0vl8>Bn}K1sQKf*lQ^kOuAd0LQ01f)Q0n@^L;Q5dM(~*OWVWQXoWd@F|#sfVVe| zmS8;XmvvDv1%hrm=t5%YqET31&`gbzf)x^1M&l?_z(;^XbS=`JO4P~#Jd(HvS@)TA zR!5x)g5n4kR7(O(UjQ*HUsi1wiAq3*=1hridQ!*(91snX>;i%b3bP+s47&v@8Y|@T zj{z-#fC~VPPyvvssN!cWcf+fbUV zuAtu~VT{V894eDq3+5ptdie_g0mvZ$sYPrE2{u`hzzf%DQF#So2uuXz+a>5jAa=K} zOVB~aeWpvWWC?bi?h>qPMV`OL{JI1!NqK{A!62~#vySnb&Pw0>EZwt41#rcws&TtcAC>M)pDj z!W7-|QaG;qL|?i>SlIlIt3)!y=JhTTkEE>j6{F>R>ul?iv4%6&@W#5x?&Si(30HC7i^5|KKs}o%~)}5qAn5Qi@GC^r*Qic-pJvN?-&;G?iIc9)$a4Xv%N9@ zQc){c)XJOMczyfT8eZR%vf7rcot(83D2U>zfh*jv3Z|r%G!RPjaR0F&$7^ zQ=iy#rS|=X%MDkH-ficrx5g}~(uQOWSK2bCyH;8e)2)~bSjWL%kFigj`1Mollh3fO zarT*$tj)7%o&W%5YczmZcy34h@F%9q#HM8bTfNIwwTYg$d*AGRE+`r+;cob5ZrZ+M(NdYs#EoNGJ5*FDLX4$nSzONW)Mi#{eY-I;7oR$P5z zVaq}@n>VmX46c-|PmCse`Laz>Yl+<=@xyi&{ zzNjtoIM8V-TQY9vjN5-U^7E5FKKYL`eBa^4p<_!!&u~M)<7fFHFW2YgjgyhxqUMxw zcv-w>5wA)aOTXrhD@ODAhS`Q#!8s=&V=Ij8j;f;t@V_g182)uH?q9YyB+e`~Zs8iY z@OCh?<t z?*y^hs}il8y*+Z^|M5W#yfc6PNMQLk(XDkXm34Dv-MqC2JfS)7{NU_hY|HEY@l)|V zbA7zIHZp+3P;#*((Umxy)Fw_R+W68=Br@cz*1;%jW#=N^h1?`~)|mU;Ok{7Ws422H zdL-(O9*Gr1pLp@{l)XIeft6E;~x*_s#9Qus?EexyYI9;EQ^}z=|6Zr?_G#i#wLh`RDh| z?mM?X-o}|5BLgsEfqh;(r;Y5(ZBy*f(_G)vyz!Z|9U-FPOnhPkV;s=CdXU^^LU8s@wr7B|4=Bw9rk@tv0v%K9CaS(i~ui*2tPcqubea9c;%g zcK1*w;Syy%G*u`Us{x_aviL%uCuEarj$GhYG z@P8`45kM`kZc^X5w!yV~lCWG{I)8fZbliPmCTdPq)ke*+BQbaENZb~$iQ8hwIHKZO zdD~SpSH6wiahNMV%n~K3;>KhvSG*}|me)-8b}o0&pNx?`uye8G$8>QWZ?1|AfD)QZ z;)SfKlEo{(fJGp->J}Ng7lvXLU-ZJ>8E%oK@1W$qMaEzgD7dlakxELW*Me4*GIR@yh4M9&vhf-26mKqgMGWW(1eCc5fGL*21 zF_CKVR?vof^^rKBxW$$)!^^&Zd_WL(qdF}eoU;6C5H*dT*e@F`8 zh(Q}fb%^jxw0b7m`aJ1yREZpy%HK&Nu{n)2+Pu(3C=oaL5L}SrGXX(6O8Wy4i=aHd z0PMvz?h%M3p`6}FJ%@rNG`jENw4f+N;H21{_CpB-6=)A^XQe2dpg!FR|6TCkEf_?Q zXsatgPzK^6RAAa03<#DWM0MQh7)It(+C&HBAVoR|V||YDjGu(~TY6g^u)vtBc8~P4 zh#YhUM@$T0IsufR?U}UKs=w5Bv5mLFB4Rn;HgB9W#y4E*yV%F)H;Z#P%nbWDdqd;^ zSh%t1JMMVF>;18D&QKZIl`<6G^;=pD5OV}LvLk$cb!6YN&4IRh@yC+tPLuY#LMs<%K(751Wp1)$KQsLkP2KFcr`qT*Gd|5a9

Ut0q=wsWiq#M*nV14u2Yg>{T>&FvtAf|49^g>{dbF|4{B zWMl}KuVEHhwf4xhY9L@{*2^=xxN^{;)e{5v?HTnL4s$gz-3SHJZ2ak^*vdlW#!t_F zI)Co7kFI?7_IIxT?8nfu+frkS6D5C=fKG{s7$~ z=HN_7Pz}*_s9ilIPBWaFq~qM_%#2(T5`*&eKGgOxlXR?KkuJb-bZI;l7d^ z1R%LAMV!8Cn=hLyi$BU2)bqymtYLlPR8BmJ!TTTf2lU&g-#mR~=s9 z^3#`Z{PcU(PgZ~S(T9==|Ndvu>o30Y*?a#`eWGI+6|XG3cKxFdu3vcV#vAW_nt0{< zD<6IOy{o_f*(_~@{+=KH`Okm8ISe_`#yuy&5a1kkJroF9IN}+nBqK3NHj0*Df*u{i zfEm$eQMZ>-@=a8(hBHSsoQtYl^Z+Uxgd~7^D2!8D_BNnulst}%F8~=>_U74R1LwSv zM_246aeI6u(fnI`TU5Uc(c{AjO=9@B#huYdm#g|eZv6FTSU|GJpXRHc;mj4$fmk<+ zmZq$Z*mLJzNR?H^^r@){D6=IN4szP=lNUFpiAej^b&rB@eQf0+Ff&Ao}Ckq9(${B^o^@A(eUa78Re>9|K z(A5H5+Br?5pC#RV^Y3qb>&GBR@Z2EZ&tCdyW#K0`F8@UG$yyq<5sb&&-f3!prv0>H z2PN)@G}1=OByWIRzA1VP`UH@kgSaFywZR9ClBbcF`XK{i(i<~pKFfI}Y-talw}B-# zfZ@UsQuADM`~+`n5cdxdRYNQK=aU7C)|SZLYbJZ-af#^}YXXoNrvEp5tY&(yU$V)o zl`zBrR@oIq2QsrB*b!}qXa(A2u6Y>km5ji`I$r^_fUgqG`bjYlJZUg(~%N7RfEoA zLFb>Md_5$M!X|!-)X6W?!$-Qd!P^ftRm(go1OI*3~YX~8H~p~3*1gF{Ng1S74X1XbuVCXy`5 zLLjzH!aW)<)kj}}CLj+iFb~NsjY_5Z9p?BQX8Rpx6925XaH9(UydDiKg_Lr9OIM$x_c*>Um2;WH5yrm+)c^ zFOHj&yBF%&f#WP*yojGjnF^Ln6`ZMpH&sUVT(jEGhiAi)z5i-)CYyLm&x?bR-LnO^ JtXNuB{|f>twsimi literal 0 HcmV?d00001 diff --git a/__pycache__/grid_near_three.cpython-312.pyc b/__pycache__/grid_near_three.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9093f860fc4d4d252edf7f02be29d38e2015cfd6 GIT binary patch literal 4611 zcmb^!No*U}^$$6uhT^7$;wDn0R-2McdP$?ikrg{pWyi6VwCNHkaoI6vWRc>c%#18i z_;hke00t0X3I!^$X)C%dU^dCY?I{HcwCJ%tlnk9p9_wHrxfn$OHV#^}7x(=khooKC zNt_O(`R~2|-hcc1!#|nL?FgPr2mR4sdl34VAnHTcfji#^U=wkOqvB|;wo-GHf@|iq z%1Y1aIL(`APS3Y-+Qt0{33iU2V>F1@^UQlx?W8#a;O(4_H@t&>qEV?7N>gbnpubI+ zW+R#!ukvQ#t%d(PCV)0k6lFA0{VarHt(3N`Ez(YPx4B}5)4bn|n4yK+-Q-ds z;HEkHEe)re*SLV#(9+%_2laaY^FQT`EpiXjr@2p`sYUK#`tFxAV*@tfc5KE*PXA*a z)?n=<7@MQk&(NffEm#xt-ETekStm`Sa~V>*QoqUr)l#o~3X#)DRJbIt}0-RCTe(~8w2pVNI0?tW*Z``wQqPvNHg|EK%i zU$OgpTPpoQ-QWAr?)N~=d8NRAM;R*luk}3jWqeRiKBc~_N?!br5IKZ>jmXoFFz;wJ z|6g_9VNY#FNnNVfu~8Ljt~H){rC%}gMR;2!ln(1}ks5EFtU|Gn3N=^U&skcg?LbRz zA2j_28ZjBf-UoJVANJ!x?3-^yk3#EzgD&;P4mGu^5ZCc0dcV3V^D90}z1jsD~A-Mg|i$@5$|vcN|c!||vntsOq_rJvixU;%tbk;bPCAcRV; z-#g?6-$WQbwnzwGLIKUJUTv?GC`iI_)B#zD>ThZbPu{23)cx zCCY6iP+U}y89vF0D^Y1drk7aqj-{eWSBvIC;j&QOjnvy#Np5?+zuqTBh@zgR< zlS^_NE3$&Xre(ddBg%AwXOprPvI{SERi;6=DC?96$a;2Z3AD+&sKh7W>^Ps44SW)! zB(M@Mw5;85O1SzmjX3n!BA0{RnBdNp^L{mZ91YTT@gQ*OZ zU3fkk=WCGSOoz%z_7sYlKs8^8x7wu$Ck#Mrl+@R%ixgI^*hrnp9ks%eQp=GA&=`?q z)A`gg1TC!8@I6>sAax>lC1&CW!)->K+Mw0Vr|t9!IZmN z2{jdRJ8|kn%|RV_6g~klAgK}oGQepPn~d&ljUYgu>^h@`Q_-^0&}H&B&9 z*3LIheqyt4p58dUwYp>5pJi?tyLXJ9veC2kosWz|dk)v;%Ernj-oS-Z#f6GvB717j z*T21zU)jcaTzvimXT|qS$>iCyc3xh2d*$+_w=b3Yg4eB~UH`!i^DY1WocWWkp3RYs zk)5tUxhqgSQ0aOi`|M|WWOr`PZOj$4SKF_&S9->>GeC2?iMFka#S_;Z!R#rZ^m?~P z@*@TNyHxQ|k-p-oxP#f}_uQTbP`?~EIPV?a^^G?E{%&mRw`5P|PE}0a>ICWuR9~jR zpb~A{we@bE**NpV*(|fin6nG()+(6l+gdF<_7}Znh>2;}(btOJa(lAY-Cl2raqfDD zwqMP^`qS@a&)%{+whmOR`*wXJg-qG^wQGs8Z#rkX53TM;uHgbMJCEdayUw9HRCOYo zYpeSQPZoL#;?G^Xw!t0SXxTPeJob_8$?91O^^I3&DHNIll_l3`nHd8IM;-@k%lRQQ zP>oWkXRvw+IG)}KPM3qzJHeSUGn%!QoTJ34`E@fm3gfdSMfdfkSZ9>nfGjt&6$mv9otyhHdZ`au^r8R zomAJ_##&+c>iCuMiX)Ugwd-_epW7Q4E57iTfyoU^j>+}_jW#@QtuIzjwr`v;y zn8;6TPv$3!%(byf?^KR~%sbqh^BeO!_5)@60jN{^L{3YrcIVxNXRn^Va=PLQ<@6+N z{-065IDGZ!m80*zR`MS#I}Tl&`R(D~9sSMGKfLzG{?b#=mmfP*cAUx4drtSZA#d2W z5A!i^##b0k)PnlOVl&3nQL$oQo_Mz6b x>w2X`+i&y?mWCd$^h`kO-k=R7(`aezsnY0Vsr}e>daA0?=$NVj(QZZKe*jjT8-4%) literal 0 HcmV?d00001 diff --git a/__pycache__/point_cloud_layout.cpython-312.pyc b/__pycache__/point_cloud_layout.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..947e9f9c985f47760c02d4d142ee6d1cb56a8616 GIT binary patch literal 48442 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 h;Ho{ry%}0-ott+P&{r62za-i}WZ{z+HhPh$(_1 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 mO|Rx2go*Y6iT?vl{@$_x literal 0 HcmV?d00001 diff --git a/__pycache__/print_mplot3d_point_cloud_layout.cpython-312.pyc b/__pycache__/print_mplot3d_point_cloud_layout.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b22d87d0865bffa5f0e4a3361427cf3786a4a9d GIT binary patch literal 59514 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 literal 0 HcmV?d00001 diff --git a/__pycache__/print_show_weight_max_obj.cpython-312.pyc b/__pycache__/print_show_weight_max_obj.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ae623c52256a1e00c021e6687edbb0dbb1fee86 GIT binary patch literal 32340 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 P{76-P%OF#g&<_2-qjAN( literal 0 HcmV?d00001 diff --git a/__pycache__/test_load_json.cpython-312.pyc b/__pycache__/test_load_json.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa44a269231985d3046323bfb74968ac07c56bde GIT binary patch literal 16905 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 0: + min_bound = np.min(points, axis=0) + max_bound = np.max(points, axis=0) + extent = max_bound - min_bound + + # 确保最小维度至少为0.01 + min_dimension = max(0.01, np.min(extent)) + volume = min_dimension ** 3 + else: + volume = 1.0 # 最后的安全回退 + + print(f"Warning: Zero volume detected, using approximated volume {volume:.6f} for {obj_path}") + + # 安全计算密度 - 防止除零错误 + if len(pcd.points) > 0 and volume > 0: + original_density = len(pcd.points) / volume + voxel_size = max(0.01, min(10.0, 0.5 / (max(1e-6, original_density) ** 0.33))) + else: + # 当点数为0或体积为0时使用默认体素大小 + voxel_size = 1.0 # 默认值 + + print(f"Recalculated voxel_size: {voxel_size} for {obj_path}") + + pcd_downsampled = down_sample(pcd, voxel_size) + pcd_downsampled.paint_uniform_color([0, 0, 1]) + + original_num = len(pcd.points) + target_samples = 1000 + num_samples = min(target_samples, original_num) + + # print("get_lowest_position_of_center1 time", time.time()-start_time1) + start_time2 = time.time() + # 确保下采样后有点云 + if len(np.asarray(pcd_downsampled.points)) == 0: + # 使用原始点云作为后备 + pcd_downsampled = pcd + print(f"Warning: Using original point cloud for {obj_path} as downsampling produced no points") + + points = np.asarray(pcd_downsampled.points) + + # 初始化最小重心Y的值 + max_z_of_mass_y = float('inf') + best_angle_x, best_angle_y, best_angle_z = 0, 0, 0 + best_angle_x, best_angle_y, best_angle_z, max_z_of_mass_y = parallel_rotation(points, angle_step=3) + + # 使用最佳角度进行旋转并平移obj + pcd_transformed = copy.deepcopy(mesh_obj) + + # 最佳角度旋转 + R_x = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([1, 0, 0]) * np.radians(best_angle_x)) + pcd_transformed.rotate(R_x) + R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * np.radians(best_angle_y)) + pcd_transformed.rotate(R_y) + R_z = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 0, 1]) * np.radians(best_angle_z)) + pcd_transformed.rotate(R_z) + + T_x = np.eye(4) + T_x[:3, :3] = R_x + center_point = compute_mesh_center(mesh_obj.vertices) + T_center_to_origin = np.eye(4) + T_center_to_origin[:3, 3] = -center_point + T_origin_to_center = np.eye(4) + T_origin_to_center[:3, 3] = center_point + T_rot_center = T_origin_to_center @ T_x @ T_center_to_origin + total_matrix = T_rot_center @ total_matrix + + T_y = np.eye(4) + T_y[:3, :3] = R_y + center_point = compute_mesh_center(mesh_obj.vertices) + T_center_to_origin = np.eye(4) + T_center_to_origin[:3, 3] = -center_point + T_origin_to_center = np.eye(4) + T_origin_to_center[:3, 3] = center_point + T_rot_center = T_origin_to_center @ T_y @ T_center_to_origin + total_matrix = T_rot_center @ total_matrix + + T_z = np.eye(4) + T_z[:3, :3] = R_z + center_point = compute_mesh_center(mesh_obj.vertices) + T_center_to_origin = np.eye(4) + T_center_to_origin[:3, 3] = -center_point + T_origin_to_center = np.eye(4) + T_origin_to_center[:3, 3] = center_point + T_rot_center = T_origin_to_center @ T_z @ T_center_to_origin + total_matrix = T_rot_center @ total_matrix + + #试着旋转180,让脸朝上 + + vertices = np.asarray(pcd_transformed.vertices) + # 计算平移向量,将最小Y值平移到0 + min_z = np.min(vertices[:, 2]) + translation_vector = np.array([0,0,-min_z,]) + pcd_transformed.translate(translation_vector) + + T_trans1 = np.eye(4) + T_trans1[:3, 3] = translation_vector + total_matrix = T_trans1 @ total_matrix + + # 计算 z 坐标均值 + vertices = np.asarray(pcd_transformed.vertices) + z_mean1 = np.mean(vertices[:, 2]) + z_max1 = np.max(vertices[:, 2]) + start_time_v1 = time.time() + volume_centroid = get_volume_centroid(pcd_transformed) + z_volume_center1 = volume_centroid[2] + delta = time.time() - start_time_v1 + # print(f"get_volume_centroid time={delta}") + + angle_rad = np.pi + #print("旋转前质心:", pcd_transformed.get_center()) + #print("旋转前点示例:", np.asarray(pcd_transformed.vertices)[:3]) + R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * angle_rad) + pcd_transformed.translate(-center_point) + pcd_transformed.rotate(R_y, center=(0, 0, 0)) + pcd_transformed.translate(center_point) + + aabb = pcd_transformed.get_axis_aligned_bounding_box() + # center_point = aabb.get_center() + center_point = compute_mesh_center(mesh_obj.vertices) + # 构建绕中心点旋转的变换矩阵[3](@ref) + T_center_to_origin = np.eye(4) + T_center_to_origin[:3, 3] = -center_point + R_y180 = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * angle_rad) + T_rotate = np.eye(4) + T_rotate[:3, :3] = R_y180 + T_origin_to_center = np.eye(4) + T_origin_to_center[:3, 3] = center_point + T_rot_center = T_origin_to_center @ T_rotate @ T_center_to_origin + total_matrix = T_rot_center @ total_matrix + + #print("旋转后质心:", pcd_transformed.get_center()) + #print("旋转后点示例:", np.asarray(pcd_transformed.vertices)[:3]) + + # + vertices = np.asarray(pcd_transformed.vertices) + # 计算平移向量,将最小Y值平移到0 + min_z = np.min(vertices[:, 2]) + max_z = np.max(vertices[:, 2]) + # print("min_z1", min_z, obj_path) + translation_vector = np.array([0,0,-min_z,]) + # translation_vector = np.array([0,0,-min_z + (min_z-max_z),]) + # print("translation_vector1",translation_vector) + pcd_transformed.translate(translation_vector) + + T_trans2 = np.eye(4) + T_trans2[:3, 3] = translation_vector + translation = total_matrix[:3, 3] + # print("translation_vector2",translation_vector) + # print(1,translation) + + total_matrix = T_trans2 @ total_matrix + translation = total_matrix[:3, 3] + # print(2,translation) + + # 计算 z 坐标均值 + vertices = np.asarray(pcd_transformed.vertices) + z_mean2 = np.mean(vertices[:, 2]) + z_max2 = np.max(vertices[:, 2]) + volume_centroid = get_volume_centroid(pcd_transformed) + z_volume_center2 = volume_centroid[2] + + # print(f"get_lowest_position_of_center z_max1={z_max1}, z_max2={z_max2}, len={len(pcd_transformed.vertices)}, obj_path={obj_path}") + + # print(f"z_mean1={z_mean1}, z_mean2={z_mean2}") + # if (z_mean2 > z_mean1): + print(f"z_volume_center1={z_volume_center1}, z_volume_center2={z_volume_center2}") + if (z_volume_center2 > z_volume_center1): + R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * -angle_rad) + centroid = pcd_transformed.get_center() + + aabb = pcd_transformed.get_axis_aligned_bounding_box() + # center_point = aabb.get_center() + center_point = compute_mesh_center(mesh_obj.vertices) + + pcd_transformed.translate(-center_point) + pcd_transformed.rotate(R_y, center=(0, 0, 0)) + pcd_transformed.translate(center_point) + + T_center_to_origin = np.eye(4) + T_center_to_origin[:3, 3] = -center_point + T_origin_to_center = np.eye(4) + T_origin_to_center[:3, 3] = center_point + # 构建反向旋转矩阵 + R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * -angle_rad) + T_rotate_inv = np.eye(4) + T_rotate_inv[:3, :3] = R_y + # 完整的反向绕中心旋转矩阵 + T_rot_center_inv = T_origin_to_center @ T_rotate_inv @ T_center_to_origin + total_matrix = T_rot_center_inv @ total_matrix + + vertices = np.asarray(pcd_transformed.vertices) + # 计算平移向量,将最小Y值平移到0 + min_z = np.min(vertices[:, 2]) + # print("min_z2", min_z, obj_path) + translation_vector = np.array([0,0,-min_z,]) + pcd_transformed.translate(translation_vector) + + T_trans3 = np.eye(4) + T_trans3[:3, 3] = translation_vector + total_matrix = T_trans3 @ total_matrix + + # z_mean_min = min(z_mean1, z_mean2) + z_max = min(z_max1, z_max2) + + # print("get_lowest_position_of_center2 time", time.time()-start_time2) + return total_matrix, z_max + +def get_lowest_position_of_center_ext(mesh_obj, total_matrix): + # print(f"get_lowest_position_of_center_ext {obj_path}") + temp_matrix, z_max = get_lowest_position_of_z_out(mesh_obj) + + total_matrix = temp_matrix @ total_matrix + + return total_matrix, z_max + +def calculate_rotation_and_top_of_mass(angle_x, angle_y, angle_z, points): + """计算某一组旋转角度后的重心""" + # 计算绕X轴、Y轴和Z轴的旋转矩阵 + R_x = np.array([ + [1, 0, 0], + [0, np.cos(np.radians(angle_x)), -np.sin(np.radians(angle_x))], + [0, np.sin(np.radians(angle_x)), np.cos(np.radians(angle_x))] + ]) + + R_y = np.array([ + [np.cos(np.radians(angle_y)), 0, np.sin(np.radians(angle_y))], + [0, 1, 0], + [-np.sin(np.radians(angle_y)), 0, np.cos(np.radians(angle_y))] + ]) + + R_z = np.array([ + [np.cos(np.radians(angle_z)), -np.sin(np.radians(angle_z)), 0], + [np.sin(np.radians(angle_z)), np.cos(np.radians(angle_z)), 0], + [0, 0, 1] + ]) + + # 综合旋转矩阵 + R = R_z @ R_y @ R_x + + # 执行旋转 + rotated_points = points @ R.T + + # 计算最小z值 + min_z = np.min(rotated_points[:, 2]) + + # 计算平移向量,将最小Z值平移到0 + translation_vector = np.array([0, 0, -min_z]) + rotated_points += translation_vector + + top_of_mass = np.max(rotated_points, axis=0) + + return top_of_mass[2], angle_x, angle_y, angle_z + +def parallel_rotation(points, angle_step=4): + """仅绕 Y 轴旋转(假设 X/Z 轴不影响目标函数)""" + max_top = float('inf') + + for angle_x in range(-90, 90, angle_step): + for angle_y in range(0, 360, angle_step): + max_z, ax, ay, _ = calculate_rotation_and_top_of_mass(angle_x, angle_y, 0, points) + if max_z < max_top: + max_top = max_z + best_angle_x = ax + best_angle_y = ay + + return (best_angle_x, best_angle_y, 0, max_top) + +def compute_mesh_center(vertices): + + if len(vertices) == 0: + raise ValueError("顶点数组不能为空") + + # 确保vertices是NumPy数组 + vertices_np = np.asarray(vertices) + + # 使用NumPy的mean函数直接计算均值(向量化操作) + centroid = np.mean(vertices_np, axis=0) + + return centroid + +# -------------------------- 结束:获取z值最低 -------------------------- + +# -------------------------- 开始:bbox -------------------------- + +def get_models_bbox(dict_pcd_fix): + """ + 单独提取:从dict_fix中解析所有模型的包围盒(bbox)尺寸信息 + :param dict_fix: 包含PLY文件名和对应点云的字典 + :return: 模型列表(包含name和dimensions) + """ + all_models = [] + extend_dist = 2 # 尺寸扩展量(单位:厘米) + for ply_file in dict_pcd_fix: + # 解析PLY文件名中的尺寸信息(格式:"模型ID=维度1+维度2+维度3.ply") + bbox_with_text = ply_file.split("=") + bbox_with = bbox_with_text[-1] + split_text = bbox_with.replace(".ply", "").split("+") + + # 转换单位:米 → 厘米 → 加扩展量 → 转回米(int取整避免浮点数精度问题) + x_length = int(float(split_text[2]) * 100) + extend_dist # 第三个维度→x方向 + y_length = int(float(split_text[0]) * 100) + extend_dist # 第一个维度→y方向 + z_length = int(float(split_text[1]) * 100) + extend_dist # 第二个维度→z方向 + + all_models.append({ + 'name': ply_file, + 'dimensions': (int(x_length / 100), int(z_length / 100), int(y_length / 100)) # 单位:米 + }) + return all_models + +def arrange_models_on_platform(models, machine_size): + """ + 单独提取:将模型在打印平台上进行排版布局 + :param models: 由get_models_bbox返回的模型列表(包含name和dimensions) + :param machine_size: 打印机尺寸 (width, depth, height) + :return: (placed_models, unplaced_models) - 已放置和未放置的模型列表 + """ + # 初始化打印平台 + platform = Platform( + int(machine_size[0]), + int(machine_size[1]), + int(machine_size[2]) + ) + print("开始计算排序...") + platform.arrange_models(models) + platform.print_results() + return platform.get_result() + +import os + +def compute_bbox_all_ext(base_original_obj_dir,compact_min_dis=True): + + obj_id_list = [aa.split(".o")[0] for aa in os.listdir(base_original_obj_dir) if aa.endswith(".obj")] + obj_id_list = obj_id_list + + dict_mesh_obj = {} + for pid_t_y in obj_id_list: + + obj_name = pid_t_y+".obj" + obj_path = os.path.join(base_original_obj_dir,obj_name) + mesh_obj = read_mesh(obj_path) + + if mesh_obj is None: + continue + + dict_mesh_obj[obj_name] = mesh_obj + + return compute_bbox_all(dict_mesh_obj,compact_min_dis) + +def compute_bbox_all(dict_mesh_obj,compact_min_dis): + dict_total_matrix= {} + dict_pcd_fix= {} + for key, value in dict_mesh_obj.items(): + start_time = time.time() + + obj_name = key + mesh_obj = value + + total_matrix, pcd_fix, ply_name = compute_bbox(mesh_obj,obj_name,compact_min_dis) + + dict_total_matrix[obj_name] = total_matrix + dict_pcd_fix[ply_name] = pcd_fix + + print(f"compute_bbox {obj_name} time={time.time()-start_time}") + + all_models = get_models_bbox(dict_pcd_fix) + + return dict_total_matrix,all_models + +def compute_bbox(mesh_obj,obj_name,compact_min_dis): + + total_matrix = np.eye(4) + total_matrix, z_min= get_lowest_position_of_center_ext(mesh_obj, total_matrix) + + transformed_vertices = mesh_transform_by_matrix(np.asarray(mesh_obj.vertices), total_matrix) + + obj_transformed = copy.deepcopy(mesh_obj) + obj_transformed.vertices = o3d.utility.Vector3dVector(transformed_vertices) + + voxel_size = 3 # 设置体素的大小,决定下采样的密度 + # 将点云摆正和X轴平衡 + obj_transformed_second,total_matrix = arrange_box_correctly(obj_transformed,voxel_size,total_matrix) + + obj_transformed,total_matrix,pcd_fix,ply_name = get_new_bbox(obj_transformed_second,obj_name,voxel_size,compact_min_dis,total_matrix) + + return total_matrix,pcd_fix,ply_name + +def arrange_box_correctly(obj_transformed, voxel_size,total_matrix): + + vertices = np.asarray(obj_transformed.vertices) + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(vertices) + + # 降采样与特征计算 + pcd_downsampled = down_sample(pcd, voxel_size) + + points = np.asarray(pcd_downsampled.points) + cov = np.cov(points.T) + + center = obj_transformed.get_center() + + # 特征分解与方向约束(关键修改点) + eigen_vals, eigen_vecs = np.linalg.eigh(cov) + max_axis = eigen_vecs[:, np.argmax(eigen_vals)] + + # print("max_axis", max_axis) + # 强制主方向向量X分量为正(指向右侧) + if max_axis[0] < 0 or (max_axis[0] == 0 and max_axis[1] < 0): + max_axis = -max_axis + + target_dir = np.array([1, 0]) # 目标方向为X正轴 + current_dir = max_axis[:2] / np.linalg.norm(max_axis[:2]) + dot_product = np.dot(current_dir, target_dir) + + # print("dot_product", dot_product) + if dot_product < 0.8: # 阈值控制方向敏感性(建议0.6~0.9) + max_axis = -max_axis # 强制翻转方向 + + # 计算旋转角度 + angle_z = np.arctan2(max_axis[1], max_axis[0]) % (2 * np.pi) + + if max_axis[0] <= 0 and max_axis[1] <= 0: + angle_z += np.pi + + R = o3d.geometry.get_rotation_matrix_from_axis_angle([0, 0, -angle_z]) + + T = np.eye(4) + T[:3, :3] = R + T[:3, 3] = center - R.dot(center) # 保持中心不变 + obj_transformed.transform(T) + + total_matrix = T @ total_matrix + + return obj_transformed, total_matrix + +def get_new_bbox(obj_transformed_second,obj_name,voxel_size,compact_min_dis,total_matrix): + + # 计算点云的边界 + points = np.asarray(obj_transformed_second.vertices) + + min_bound = np.min(points, axis=0) # 获取点云的最小边界 + max_bound = np.max(points, axis=0) # 获取点云的最大边界 + + # 确保包围盒的Y坐标不低于0 + min_bound[2] = max(min_bound[2], 0) # 确保Y坐标的最小值不低于0 + + # 重新计算包围盒的中心和半长轴 + bbox_center = (min_bound + max_bound) / 2 # 计算包围盒的中心点 + bbox_extent = (max_bound - min_bound) # 计算包围盒的半长轴(尺寸) + + # 创建包围盒,确保尺寸正确 + new_bbox = o3d.geometry.OrientedBoundingBox(center=bbox_center, + R=np.eye(3), # 旋转矩阵,默认没有旋转 + extent=bbox_extent) + # 获取包围盒的长、宽和高 + x_length = round(bbox_extent[0],3) # X 方向的长 + y_length = round(bbox_extent[1],3) # Y 方向的宽 + z_length = round(bbox_extent[2],3) # Z 方向的高 + bbox_points = np.array([ + [min_bound[0], min_bound[1], min_bound[2]], + [max_bound[0], min_bound[1], min_bound[2]], + [max_bound[0], max_bound[1], min_bound[2]], + [min_bound[0], max_bound[1], min_bound[2]], + [min_bound[0], min_bound[1], max_bound[2]], + [max_bound[0], min_bound[1], max_bound[2]], + [max_bound[0], max_bound[1], max_bound[2]], + [min_bound[0], max_bound[1], max_bound[2]] + ]) + first_corner = bbox_points[2] + translation_vector = -first_corner + obj_transformed_second.translate(translation_vector) + + T_trans = np.eye(4) + T_trans[:3, 3] = translation_vector # 设置平移分量 [2,3](@ref) + total_matrix = T_trans @ total_matrix # 矩阵乘法顺序:最新变换左乘[4,5](@ref) + + new_bbox.translate(translation_vector) + + vertices = np.asarray(obj_transformed_second.vertices) + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(vertices) + if compact_min_dis: + pcd_downsampled = down_sample(pcd, voxel_size, False) + pcd_fix = pcd_downsampled + else: + pcd_fix = pcd + + ply_print_pid = obj_name.replace(".obj","") + ply_name = f"{ply_print_pid}={z_length}+{y_length}+{x_length}.ply" + + return obj_transformed_second, total_matrix, pcd_fix, ply_name + +class Platform: + def __init__(self, width, depth, height): + self.width = width + self.depth = depth + self.height = height + self.placed_models = [] # 已放置的模型 + self.unplaced_models = [] # 未能放置的模型 + self.first_line = True + self.remove_multiobj_name = "" + + def is_cross_border(self, x, y, z, model): + mx, my, mz = model['dimensions'] + + return is_cross_border_c(x, y, z, mx, my, mz, self.width, self.depth, self.height) + + def check_multiobj_cross_pre(self, name, pre_model): + if (pre_model==None): + return + + if not is_same_obj(name, pre_model['name']): + return + + self.unplaced_models.append(pre_model) + if pre_model in self.placed_models: + self.placed_models.remove(pre_model) + + if "pre_model" in pre_model: + self.pre_model = pre_model["pre_model"] + + self.check_multiobj_cross_pre(name, self.pre_model) + + def check_multiobj_cross(self, model): + if not is_multi_obj(model['name']): + return False + + self.unplaced_models.append(model) + + if "pre_model" in model: + self.pre_model = model["pre_model"] + + self.check_multiobj_cross_pre(model['name'], self.pre_model) + + return True + + def can_place(self, x, y, z, model, is_print=False): + mx, my, mz = model['dimensions'] + + if self.is_cross_border(x, y, z, model): + + print(f"can_place False 1 cross_border {x}, {y}, {z}, {model}, {self.width}, {self.depth}, {self.height}") + return False + + # 碰撞检测(正确逻辑与间距处理) + for placed in self.placed_models: + px, py, pz = placed['position'] + pdx, pdy, pdz = placed['dimensions'] + + # 使用AABB碰撞检测算法[4](@ref) + if (x > px - pdx - extend_dist_model_x and + x - mx - extend_dist_model_x < px and + y > py - pdy - extend_dist_model_y and + y - my - extend_dist_model_y < py and + z < pz + pdz and + z + mz > pz): + print("can_place False 2",False,model,x,y,z,px,pdx,extend_dist_model_x,py,pdy,extend_dist_model_y,my,pz,pdz,pz) + return False + + return True + + def place_model(self, model, pre_model): + mx, my, mz = model['dimensions'] + if mz > self.height: + self.unplaced_models.append(model) + return False + + if is_same_obj(model['name'], self.remove_multiobj_name): + self.unplaced_models.append(model) + return False + + z = 0 + + if pre_model is None: + if self.first_line: + model['position'] = (mx + extend_dist_border_x_min, self.depth - extend_dist_border_y_max, 0) + print(f"First Model {model['name']}") + model['first_line'] = True + else: + model['position'] = (self.width - extend_dist_border_x_max, self.depth - extend_dist_border_y_max, 0) + model['first_line'] = False + + print("model position1", model['name'], model['position']) + self.placed_models.append(model) + return True + + pre_px, pre_py, pre_pz = pre_model['position'] + pre_mx, pre_my, pre_mz = pre_model['dimensions'] + if self.first_line: + px = pre_px + mx + extend_dist_model_x + model['first_line'] = True + else: + px = pre_px - pre_mx - extend_dist_model_x + model['first_line'] = False + print(model['name'], "px", px, pre_px, pre_mx) + + reach_limit_x = False + if self.first_line: + if px > self.width: + reach_limit_x = True + else: + if px - mx < 0: + reach_limit_x = True + + if reach_limit_x: + self.first_line = False + px = self.width - extend_dist_border_x_max + start_y = my + extend_dist_border_y_min + final_y = self.depth + print("reach_limit_x final_y1", model['name'], my, final_y, my, extend_dist_border_x_max, px) + for y in range(start_y, final_y, +1): + # print("y",y) + if self.can_place(px, y, z, model, True)==False: + y -= 1 + + if self.is_cross_border(px, y, z, model): + print(f"cross border : {model['name']}") + if self.check_multiobj_cross(model): + self.remove_multiobj_name = model['name'] + return False + + model['position'] = (px, y, z) + print("model position2", model['name'], model['position']) + self.placed_models.append(model) + return True + else: + start_y = my + extend_dist_border_y_min + final_y = self.depth + print("final_y2", model['name'], start_y, final_y, my, extend_dist_border_y_max, px) + for y in range(start_y, final_y, +1): + if self.can_place(px, y, z, model)==False: + y -= 1 + + if self.is_cross_border(px, y, z, model): + print(f"cross border : {model['name']}") + if self.check_multiobj_cross(model): + self.remove_multiobj_name = model['name'] + return False + + model['position'] = (px, y, z) + print("model position2", model['name'], model['position']) + self.placed_models.append(model) + return True + + if 'position' in model: + print("model position3", model['name'], model['position']) + else: + print("model position3 no exist position", model['name']) + + self.unplaced_models.append(model) + return False + + def arrange_models(self, models): + + """对所有模型进行排布(单层)""" + print("⚠️ 单层放置模式:所有模型只能放在平台底面(Z=0)") + # 按高度和面积排序,优先放大模型 + models = sorted(models, key=lambda m: (-m['dimensions'][2], -m['dimensions'][0] * m['dimensions'][1])) + self.pre_model = None + for model in models: + print(f"arrange_models {model['name']}") + pre_model_temp = self.pre_model + if self.place_model(model, self.pre_model): + self.pre_model = model + + model["pre_model"] = pre_model_temp + + def print_results(self): + """打印排布结果""" + print("Placed Models:") + for model in self.placed_models: + print(f" - {model['name']} at {model['position']} with dimensions {model['dimensions']}") + print("Unplaced Models:") + for model in self.unplaced_models: + print(f" - {model['name']} with dimensions {model['dimensions']}") + + def get_result(self): + return self.placed_models, self.unplaced_models + +# -------------------------- 结束:bbox -------------------------- + +if __name__ == '__main__': + + parser = argparse.ArgumentParser() + parser.add_argument("--obj_path", type=str, required=True, help="batchobj_path_id") + args = parser.parse_args() + + obj_path = args.obj_path + max, z = get_lowest_position_of_z_out(obj_path) + diff --git a/config.py b/config.py new file mode 100644 index 0000000..3623e44 --- /dev/null +++ b/config.py @@ -0,0 +1,18 @@ +from enum import Enum +class run_mode(Enum): + formal = 1 + test = 2 + fix = 3 + +# !!!谨慎选择run_mode.fix,这将把修复的数据上传到正式的系统 !!! +curr_run_mode = run_mode.test + +is_test = True + +# print_factory_type_dir="/root/print_factory_type" +print_factory_type_dir="/home/algo/Documents/print_factory_type" + +url_send_layout = 'https://mp.api.suwa3d.com/api/printTypeSettingOrder/printTypeSettingOrderSuccess' + +big_machine_size = [600, 500, 300] +small_machine_size = [380, 345, 250] diff --git a/download_print.py b/download_print.py index 4b79924..a34ed3a 100644 --- a/download_print.py +++ b/download_print.py @@ -13,7 +13,10 @@ import math import os import argparse -is_test = False +from config import print_factory_type_dir +from config import is_test + +from general import is_use_debug_oss CameraModel = collections.namedtuple( "CameraModel", ["model_id", "model_name", "num_params"] @@ -1399,12 +1402,8 @@ def download_transform_save_by_batch(batch_id, workdir, oss_config): layout_data = datas["layout_data"] original_obj_pid_dir = workdir - cache_type_setting_dir = os.path.join(workdir, "arrange") - Path(cache_type_setting_dir).mkdir(exist_ok=True) - - print(f"original_obj_pid_dir={original_obj_pid_dir}, cache_type_setting_dir={cache_type_setting_dir}") - transform_save(layout_data, original_obj_pid_dir, cache_type_setting_dir) + transform_save_o3d(layout_data, original_obj_pid_dir) def download_datas_by_json(pid_file, workdir, oss_config): @@ -1427,12 +1426,8 @@ def download_transform_save_by_json(pid_file, workdir, oss_config): layout_data = download_datas_by_json(pid_file, workdir, oss_config) original_obj_pid_dir = workdir - cache_type_setting_dir = os.path.join(workdir, "arrange") - Path(cache_type_setting_dir).mkdir(exist_ok=True) - - print(f"original_obj_pid_dir={original_obj_pid_dir}, cache_type_setting_dir={cache_type_setting_dir}") - transform_save(layout_data, original_obj_pid_dir, cache_type_setting_dir) + transform_save_o3d(layout_data, original_obj_pid_dir) def upload_result(base_original_obj_dir, oss_config, batch_id): @@ -1442,7 +1437,9 @@ def upload_result(base_original_obj_dir, oss_config, batch_id): target_dir = f"{base_original_obj_dir}" oss_batch_dir = "batchPrint" - if is_test: + print(f"is_use_debug_oss={is_use_debug_oss()}") + if is_use_debug_oss(): + # if is_test: oss_batch_dir = "batchPrint/debug_hsc" print(f"target_dir={target_dir}, batch_id={batch_id}") @@ -1456,52 +1453,6 @@ def upload_result(base_original_obj_dir, oss_config, batch_id): pass import open3d as o3d -from test_load_json import custom_mesh_transform - -def transform_save(layout_data, original_obj_pid_dir, cache_type_setting_dir): - meshes = [] - # 小打印机380*345,需要偏移-380,-345 - need_offset = True - for model in layout_data["models"]: - transform = model.get('transform', {}) - - homo_matrix = transform["homo_matrix"] # 获取存储的列表 - reconstructed_matrix = np.array(homo_matrix, dtype=np.float64) - - obj_name = model.get('file_name', '') - obj_path = os.path.join(original_obj_pid_dir, obj_name) - # 加载网格 - try: - mesh = o3d.io.read_triangle_mesh(obj_path, enable_post_processing=True) - if not mesh.has_vertices(): - print(f"警告: 网格无有效顶点 - {obj_path}") - continue - except Exception as e: - print(f"加载模型失败: {obj_path} - {e}") - continue - - original_vertices = np.asarray(mesh.vertices) - - transformed_vertices = custom_mesh_transform(original_vertices, reconstructed_matrix) - # 如果 need_offset 为 True,应用额外的偏移 - if need_offset: - # 应用偏移 (-380, -345, 0) - offset = np.array([-380, -345, 0]) - transformed_vertices += offset - print(f"已对模型 {obj_name} 应用偏移: {offset}") - - mesh.vertices = o3d.utility.Vector3dVector(transformed_vertices) - - meshes.append(mesh) - - # obj_path_arrange = os.path.join(original_obj_pid_dir, "arrange") - obj_path_arrange = cache_type_setting_dir - if not os.path.exists(obj_path_arrange): - os.mkdir(obj_path_arrange) - obj_path_arrange_obj = os.path.join(obj_path_arrange, obj_name) - print("obj_path_arrange_obj", obj_path_arrange_obj) - mesh.compute_vertex_normals() - o3d.io.write_triangle_mesh(obj_path_arrange_obj, mesh,write_triangle_uvs=True) if __name__ == "__main__": parser = argparse.ArgumentParser() @@ -1517,9 +1468,8 @@ if __name__ == "__main__": args = parser.parse_args() """ # batch_id = args.batch_id - batch_id = 2499 + batch_id = 10118 - print_factory_type_dir="/root/print_factory_type" # workdir = args.workdir workdir = f"{print_factory_type_dir}/{batch_id}" diff --git a/download_print_out.py b/download_print_out.py index 26626e6..b9a29c6 100644 --- a/download_print_out.py +++ b/download_print_out.py @@ -1,33 +1,22 @@ import yaml import oss2 -import os from tqdm import tqdm import os -from pathlib import Path import numpy as np import os import argparse - +import bpy +import sys import open3d as o3d -def custom_mesh_transform(vertices, transform_matrix): - """ - 手动实现网格变换:对每个顶点应用齐次变换矩阵 - 参数: - vertices: 网格顶点数组 (N, 3) - transform_matrix: 4x4 齐次变换矩阵 - 返回: - 变换后的顶点数组 (N, 3) - """ - # 1. 顶点转齐次坐标 (N, 3) → (N, 4) - homogeneous_vertices = np.hstack((vertices, np.ones((vertices.shape[0], 1)))) - - # 2. 应用变换矩阵:矩阵乘法 (4x4) * (4xN) → (4xN) - transformed_homogeneous = transform_matrix @ homogeneous_vertices.T - - # 3. 转回非齐次坐标 (3xN) → (N, 3) - transformed_vertices = transformed_homogeneous[:3, :].T - return transformed_vertices +import requests +import json +import shutil + +def download_transform_save_by_json(pid_file, workdir, oss_config): + layout_data = download_datas_by_json(pid_file, workdir, oss_config) + original_obj_pid_dir = workdir + transform_save_bpy(layout_data, original_obj_pid_dir) class DataTransfer: ''' @@ -43,10 +32,6 @@ class DataTransfer: self.oss_path = oss_path.lstrip('/') self.oss_client = oss_client - order_id: str - pid: str - model_height: str - def download_data_rename_json(self, json_model_info): """ 从 OSS 下载数据到本地,保持原有目录结构 @@ -163,10 +148,6 @@ class DataTransfer: print(f"下载文件: {obj_key} -> {local_path}") -import requests -import json -import shutil - def get_api(url): try: response = requests.get(url) @@ -185,6 +166,7 @@ class JSONModelInfo: obj_name: str order_id: str pid: str + print_order_id: str model_height: str def read_pids_from_json(pid_file): @@ -217,15 +199,19 @@ def read_pids_from_json(pid_file): order_id = parts[0] pid = parts[1] + print_order_id = parts[2] + print_order_id = print_order_id.replace("P", "") model_height = parts[3] model_info = JSONModelInfo( obj_name=obj_name, order_id=order_id, pid=pid, + print_order_id=print_order_id, model_height=model_height ) list_model_info.append(model_info) + print(f"model_info={model_info}") return list_model_info, data @@ -233,7 +219,6 @@ def download_data_by_json(model_info, workdir, oss_client ): try: pid = model_info.pid model_height = model_info.model_height - # target_dir = f"{workdir}/{pid}_image" target_dir = f"{workdir}" url = f"https://mp.api.suwa3d.com/api/order/getOssSuffixByOrderId?order_id={model_info.order_id}" @@ -252,7 +237,7 @@ def download_data_by_json(model_info, workdir, oss_client ): shutil.rmtree(target_dir) print(f"下载后检查发现目标文件夹为空,已删除: {target_dir}") except Exception as e: - print(f"卡通图片下载失败: {pid}, 错误: {str(e)}") + print(f"下载失败: {pid}, 错误: {str(e)}") pass def get_oss_client(cfg_path): @@ -286,69 +271,24 @@ def download_datas_by_json(pid_file, workdir, oss_config): return data -def download_transform_save_by_json(pid_file, workdir, oss_config): - layout_data = download_datas_by_json(pid_file, workdir, oss_config) - - original_obj_pid_dir = workdir - cache_type_setting_dir = os.path.join(workdir, "arrange") - Path(cache_type_setting_dir).mkdir(exist_ok=True) - - print(f"original_obj_pid_dir={original_obj_pid_dir}, cache_type_setting_dir={cache_type_setting_dir}") - - transform_save(layout_data, original_obj_pid_dir, cache_type_setting_dir) - -def transform_save(layout_data, original_obj_pid_dir, cache_type_setting_dir): - meshes = [] - # 小打印机380*345,需要偏移-380,-345 - need_offset = True - for model in layout_data["models"]: - transform = model.get('transform', {}) - - homo_matrix = transform["homo_matrix"] # 获取存储的列表 - reconstructed_matrix = np.array(homo_matrix, dtype=np.float64) - - obj_name = model.get('file_name', '') - obj_path = os.path.join(original_obj_pid_dir, obj_name) - # 加载网格 - try: - mesh = o3d.io.read_triangle_mesh(obj_path, enable_post_processing=True) - if not mesh.has_vertices(): - print(f"警告: 网格无有效顶点 - {obj_path}") - continue - except Exception as e: - print(f"加载模型失败: {obj_path} - {e}") - continue - - original_vertices = np.asarray(mesh.vertices) - - transformed_vertices = custom_mesh_transform(original_vertices, reconstructed_matrix) - # 如果 need_offset 为 True,应用额外的偏移 - if need_offset: - # 应用偏移 (-380, -345, 0) - offset = np.array([-380, -345, 0]) - transformed_vertices += offset - print(f"已对模型 {obj_name} 应用偏移: {offset}") - - mesh.vertices = o3d.utility.Vector3dVector(transformed_vertices) +if __name__ == "__main__": - meshes.append(mesh) + script_args = [] - # obj_path_arrange = os.path.join(original_obj_pid_dir, "arrange") - obj_path_arrange = cache_type_setting_dir - if not os.path.exists(obj_path_arrange): - os.mkdir(obj_path_arrange) - obj_path_arrange_obj = os.path.join(obj_path_arrange, obj_name) - print("obj_path_arrange_obj", obj_path_arrange_obj) - mesh.compute_vertex_normals() - o3d.io.write_triangle_mesh(obj_path_arrange_obj, mesh,write_triangle_uvs=True) + try: + separator_index = sys.argv.index("--") + script_args = sys.argv[separator_index + 1:] + except ValueError: + pass -if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--pid_file", type=str, required=True, help="批次号, 也是json文件名") - parser.add_argument("--workdir", type=str, required=True, help="本代码文件所在的目录") - parser.add_argument("--oss_config", type=str, required=True) - args = parser.parse_args() + parser.add_argument("--workdir", type=str, required=True, help="json文件所在的目录") + parser.add_argument("--oss_config", type=str, required=True, help="run.yaml所在的目录") + args = parser.parse_args(script_args) download_transform_save_by_json(args.pid_file, args.workdir, args.oss_config) + + # blender --background --python download_print_out.py -- --pid_file {your_batch_id} --workdir {your_batch_dir} --oss_config {your_yaml_dir/run.yaml} \ No newline at end of file diff --git a/general.py b/general.py new file mode 100644 index 0000000..c5f6acb --- /dev/null +++ b/general.py @@ -0,0 +1,286 @@ +import open3d as o3d +import numpy as np +import re +from config import * + +# -------------------------- 开始:运行 ---------------------------------- + +def is_run_local_data(): + if curr_run_mode == run_mode.formal: + return False + return True + +def need_upload_result(): + if curr_run_mode == run_mode.test: + return False + return True + +def is_use_debug_oss(): + if curr_run_mode == run_mode.test: + return True + return False + +# -------------------------- 结束:运行 ---------------------------------- + +# -------------------------- 开始:模型 ---------------------------------- + +def read_mesh(obj_path): + mesh_obj = o3d.io.read_triangle_mesh(obj_path) + return mesh_obj + +def down_sample(pcd, voxel_size, farthest_sample = False): + original_num = len(pcd.points) + target_samples = 1500 # 1000 + num_samples = min(target_samples, original_num) + + # 第一步:使用体素下采样快速减少点数量 + # voxel_size = 3 + if farthest_sample: + pcd_voxel = pcd.farthest_point_down_sample(num_samples=num_samples) + else: + pcd_voxel = pcd.voxel_down_sample(voxel_size) + down_num = len(pcd_voxel.points) + # print(f"original_num={original_num}, down_num={down_num}") + + # 第二步:仅在必要时进行最远点下采样 + if len(pcd_voxel.points) > target_samples and False: + pcd_downsampled = pcd_voxel.farthest_point_down_sample(num_samples=num_samples) + else: + pcd_downsampled = pcd_voxel + + return pcd_downsampled + +def get_volume_centroid(mesh): + """向量化版本的体积重心计算""" + vertices = np.asarray(mesh.vertices) + triangles = np.asarray(mesh.triangles) + + # 一次性获取所有三角形的顶点 [n_triangles, 3, 3] + tri_vertices = vertices[triangles] + + # 分别提取三个顶点 + v0 = tri_vertices[:, 0, :] # [n, 3] + v1 = tri_vertices[:, 1, :] # [n, 3] + v2 = tri_vertices[:, 2, :] # [n, 3] + + # 向量化计算叉积和点积 + cross_vec = np.cross(v1, v2) # [n, 3] + dot_results = np.einsum('ij,ij->i', v0, cross_vec) # 批量点积 [n] + + # 计算每个四面体体积 [n] + tetra_volumes = np.abs(dot_results) / 6.0 + + # 计算每个四面体重心 [n, 3] + tetra_centers = (v0 + v1 + v2) / 4.0 + + # 总体积和加权重心 + total_volume = np.sum(tetra_volumes) + + if total_volume > 0: + weighted_center = np.sum(tetra_volumes[:, np.newaxis] * tetra_centers, axis=0) / total_volume + return weighted_center + else: + return np.mean(vertices, axis=0) + +def mesh_tranform_to_pcd(mesh, transform_matrix): + vertices = np.asarray(mesh.vertices) + + transformed_vertices = mesh_transform_by_matrix(vertices, transform_matrix) + mesh.vertices = o3d.utility.Vector3dVector(transformed_vertices) + + return mesh_to_pcd(mesh) + +def mesh_to_pcd(mesh, is_down_sample=True): + + vertices = np.asarray(mesh.vertices) + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(vertices) + + if is_down_sample: + pcd_downsampled = down_sample(pcd, voxel_size, False) + return pcd_downsampled + else: + return pcd + +def mesh_transform_by_matrix(vertices, transform_matrix): + """ + 手动实现网格变换:对每个顶点应用齐次变换矩阵 + 参数: + vertices: 网格顶点数组 (N, 3) + transform_matrix: 4x4 齐次变换矩阵 + 返回: + 变换后的顶点数组 (N, 3) + """ + # 1. 顶点转齐次坐标 (N, 3) → (N, 4) + homogeneous_vertices = np.hstack((vertices, np.ones((vertices.shape[0], 1)))) + + # 2. 应用变换矩阵:矩阵乘法 (4x4) * (4xN) → (4xN) + transformed_homogeneous = transform_matrix @ homogeneous_vertices.T + + # 3. 转回非齐次坐标 (3xN) → (N, 3) + transformed_vertices = transformed_homogeneous[:3, :].T + return transformed_vertices + +import os + +def transform_save_bpy(layout_data, original_obj_pid_dir): + # 清除场景 + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete(use_global=False) + + ## 尺寸调整,环境设置 + bpy.ops.object.delete(use_global=False, confirm=False) + bpy.context.scene.unit_settings.length_unit = 'CENTIMETERS' + bpy.context.scene.unit_settings.scale_length = 0.001 + bpy.context.scene.unit_settings.mass_unit = 'GRAMS' + + meshes = [] + need_offset = True + for model in layout_data["models"]: + transform = model.get('transform', {}) + homo_matrix = transform["homo_matrix"] + reconstructed_matrix = np.array(homo_matrix, dtype=np.float64) + + obj_name = model.get('file_name', '') + obj_path = os.path.join(original_obj_pid_dir, obj_name) + + mtl_name_temp = obj_name + separator = "_P" + index = mtl_name_temp.find(separator) + if index != -1: + old_mtl_name = mtl_name_temp[:index] + else: + old_mtl_name = mtl_name_temp # 或者你希望的其他处理逻辑,比如直接使用原字符串或报错 + old_mtl_name = f"{old_mtl_name}.mtl" + old_mtl_path = os.path.join(original_obj_pid_dir, old_mtl_name) + + bpy.ops.wm.obj_import(filepath=obj_path) + imported_object = bpy.context.object + + if imported_object is None or imported_object.type != 'MESH': + print(f"警告: 未能成功导入网格对象 {obj_name}。跳过。") + continue + + mesh_data = imported_object.data + mesh_data.calc_loop_triangles() + original_vertices = np.empty(len(mesh_data.vertices) * 3, dtype=np.float64) + mesh_data.vertices.foreach_get('co', original_vertices) + original_vertices = original_vertices.reshape(-1, 3) + + transformed_vertices = mesh_transform_by_matrix(original_vertices, reconstructed_matrix) + if need_offset: + offset = np.array([-small_machine_size[0], -small_machine_size[1], 0]) + transformed_vertices += offset + print(f"已对模型 {obj_name} 应用偏移: {offset}") + + flattened_verts = transformed_vertices.reshape(-1) + mesh_data.vertices.foreach_set('co', flattened_verts) + + mesh_data.update() + + meshes.append(imported_object) + + # 导出前确保选中当前对象 + bpy.ops.object.select_all(action='DESELECT') + imported_object.select_set(True) + bpy.context.view_layer.objects.active = imported_object + bpy.ops.wm.obj_export( + filepath=obj_path, + export_selected_objects=True, + export_materials=True, + path_mode='AUTO' + ) + + os.remove(old_mtl_path) + +def transform_save_o3d(layout_data, original_obj_pid_dir): + meshes = [] + # 小打印机需要偏移[-small_machine_size[0], -small_machine_size[1], 0] + need_offset = True + for model in layout_data["models"]: + transform = model.get('transform', {}) + + homo_matrix = transform["homo_matrix"] # 获取存储的列表 + reconstructed_matrix = np.array(homo_matrix, dtype=np.float64) + + obj_name = model.get('file_name', '') + obj_path = os.path.join(original_obj_pid_dir, obj_name) + # 加载网格 + try: + mesh = o3d.io.read_triangle_mesh(obj_path, enable_post_processing=True) + if not mesh.has_vertices(): + print(f"警告: 网格无有效顶点 - {obj_path}") + continue + except Exception as e: + print(f"加载模型失败: {obj_path} - {e}") + continue + + original_vertices = np.asarray(mesh.vertices) + + transformed_vertices = mesh_transform_by_matrix(original_vertices, reconstructed_matrix) + # 如果 need_offset 为 True,应用额外的偏移 + if need_offset: + offset = np.array([-small_machine_size[0], -small_machine_size[1], 0]) + transformed_vertices += offset + print(f"已对模型 {obj_name} 应用偏移: {offset}") + + mesh.vertices = o3d.utility.Vector3dVector(transformed_vertices) + + meshes.append(mesh) + + obj_path_arrange = os.path.join(original_obj_pid_dir, "arrange") + if not os.path.exists(obj_path_arrange): + os.mkdir(obj_path_arrange) + obj_path_arrange_obj = os.path.join(obj_path_arrange, obj_name) + print("obj_path_arrange_obj", obj_path_arrange_obj) + mesh.compute_vertex_normals() + o3d.io.write_triangle_mesh(obj_path_arrange_obj, mesh,write_triangle_uvs=True) + +voxel_size = 3 + +def is_multi_obj(obj_name): + pattern = r'_x(\d+)' + match = re.search(pattern, obj_name) + if match: + number = match.group(1) + + if int(number) > 1 : + return True + + return False + +def is_same_obj(obj_name1, obj_name2): + + pre_name1 = obj_name1.split("_x")[0] + pre_name2 = obj_name2.split("_x")[0] + + print(f"pre_name1={pre_name1}, pre_name2={pre_name2}") + + if (pre_name1==pre_name2): + return True + + return False + +# -------------------------- 结束:模型 ---------------------------------- + +# -------------------------- 开始:碰撞检测和越界 -------------------------- + +extend_dist_border_x_min = 6 +extend_dist_border_x_max = 3 +extend_dist_border_z_max = 3 +extend_dist_border_y_min = 6 +extend_dist_border_y_max = 6 +extend_dist_model_x = 4 +extend_dist_model_y = 2 +extend_dist_model_z = 2 +extend_dist_box = 1 +def is_cross_border_c(x, y, z, mx, my, mz, max_x, max_y, max_z): + if (x - mx < extend_dist_border_x_min or + y - my < extend_dist_border_y_min or + z + mz > max_z - extend_dist_border_z_max or + y > max_y - extend_dist_border_y_max): + return True + + return False + +# -------------------------- 结束:碰撞检测和越界 -------------------------- diff --git a/get_lowest_position_of_center_ext.py b/get_lowest_position_of_center_ext.py deleted file mode 100644 index f64a5b2..0000000 --- a/get_lowest_position_of_center_ext.py +++ /dev/null @@ -1,847 +0,0 @@ -import open3d as o3d -import numpy as np -import copy -import time -from get_lowest_position_of_z_out import get_lowest_position_of_z_out - -# 对外部提供的获取最低z的接口 -def get_lowest_position_of_center_out2(obj_path): - - total_matrix = np.eye(4) - - mesh_obj = o3d.io.read_triangle_mesh(obj_path) - - voxel_size = 3 - - return get_lowest_position_of_center(mesh_obj, obj_path, total_matrix, voxel_size) - -def calculate_rotation_and_center_of_mass(angle_x, angle_y, angle_z, points): - """计算某一组旋转角度后的重心""" - # 计算绕X轴、Y轴和Z轴的旋转矩阵 - R_x = np.array([ - [1, 0, 0], - [0, np.cos(np.radians(angle_x)), -np.sin(np.radians(angle_x))], - [0, np.sin(np.radians(angle_x)), np.cos(np.radians(angle_x))] - ]) - - R_y = np.array([ - [np.cos(np.radians(angle_y)), 0, np.sin(np.radians(angle_y))], - [0, 1, 0], - [-np.sin(np.radians(angle_y)), 0, np.cos(np.radians(angle_y))] - ]) - - R_z = np.array([ - [np.cos(np.radians(angle_z)), -np.sin(np.radians(angle_z)), 0], - [np.sin(np.radians(angle_z)), np.cos(np.radians(angle_z)), 0], - [0, 0, 1] - ]) - - # 综合旋转矩阵 - R = R_z @ R_y @ R_x - - # 执行旋转 - rotated_points = points @ R.T - - # 计算最小z值 - min_z = np.min(rotated_points[:, 2]) - - # 计算平移向量,将最小Z值平移到0 - translation_vector = np.array([0, 0, -min_z]) - rotated_points += translation_vector - - # 计算重心 - center_of_mass = np.mean(rotated_points, axis=0) - - return center_of_mass[2], angle_x, angle_y, angle_z - -def parallel_rotation4(points, angle_step=4): - """仅绕 Y 轴旋转(假设 X/Z 轴不影响目标函数)""" - min_center = float('inf') - - for angle_x in range(-45, 45, angle_step): - for angle_y in range(0, 360, angle_step): - center_z, ax, ay, _ = calculate_rotation_and_center_of_mass(angle_x, angle_y, 0, points) - if center_z < min_center: - min_center = center_z - best_angle_x = ax - best_angle_y = ay - - return (best_angle_x, best_angle_y, 0, min_center) - -import numpy as np -from numba import cuda -import math - -# CUDA核函数:计算所有旋转角度下的重心高度 -@cuda.jit -def compute_centers_kernel(points, centers, angle_x_start, angle_x_step, angle_y_start, angle_y_step, num_x, num_y): - i = cuda.blockIdx.x * cuda.blockDim.x + cuda.threadIdx.x - j = cuda.blockIdx.y * cuda.blockDim.y + cuda.threadIdx.y - - if i >= num_x or j >= num_y: - return - - # 获取整数角度值 - angle_x = angle_x_start + i * angle_x_step - angle_y = angle_y_start + j * angle_y_step - - rx = math.radians(float(angle_x)) # 使用 float() 进行转换 - ry = math.radians(float(angle_y)) - rz = 0.0 - - # 计算旋转矩阵 - cos_x = math.cos(rx) - sin_x = math.sin(rx) - cos_y = math.cos(ry) - sin_y = math.sin(ry) - cos_z = math.cos(rz) - sin_z = math.sin(rz) - - # 旋转矩阵: R = R_z * R_y * R_x - R00 = cos_z * cos_y - R01 = cos_z * sin_y * sin_x - sin_z * cos_x - R02 = cos_z * sin_y * cos_x + sin_z * sin_x - R10 = sin_z * cos_y - R11 = sin_z * sin_y * sin_x + cos_z * cos_x - R12 = sin_z * sin_y * cos_x - cos_z * sin_x - R20 = -sin_y - R21 = cos_y * sin_x - R22 = cos_y * cos_x - - n = points.shape[0] - min_z = 1e10 - - # 第一遍:计算最小Z值 - for k in range(n): - x = points[k, 0] - y = points[k, 1] - z = points[k, 2] - - x_rot = R00 * x + R01 * y + R02 * z - y_rot = R10 * x + R11 * y + R12 * z - z_rot = R20 * x + R21 * y + R22 * z - - if z_rot < min_z: - min_z = z_rot - - total_z = 0.0 - # 第二遍:平移并计算Z坐标和 - for k in range(n): - x = points[k, 0] - y = points[k, 1] - z = points[k, 2] - - x_rot = R00 * x + R01 * y + R02 * z - y_rot = R10 * x + R11 * y + R12 * z - z_rot = R20 * x + R21 * y + R22 * z - - z_trans = z_rot - min_z - total_z += z_trans - - center_z = total_z / n - centers[i, j] = center_z - -# CUDA版本的并行旋转计算 -def parallel_rotation4_cuda(points, angle_step=4): - angle_x_start = -45 - angle_x_end = 45 - angle_y_start = 0 - angle_y_end = 360 - - num_x = int((angle_x_end - angle_x_start) / angle_step) - num_y = int((angle_y_end - angle_y_start) / angle_step) - - # 将点云数据复制到GPU - d_points = cuda.to_device(points.astype(np.float32)) - d_centers = cuda.device_array((num_x, num_y), dtype=np.float32) - - # 配置线程块和网格 - threadsperblock = (16, 16) - blockspergrid_x = (num_x + threadsperblock[0] - 1) // threadsperblock[0] - blockspergrid_y = (num_y + threadsperblock[1] - 1) // threadsperblock[1] - blockspergrid = (blockspergrid_x, blockspergrid_y) - - # 启动核函数 - compute_centers_kernel[blockspergrid, threadsperblock]( - d_points, d_centers, angle_x_start, angle_step, angle_y_start, angle_step, num_x, num_y - ) - - # 将结果复制回主机 - centers = d_centers.copy_to_host() - - # 找到最小重心值的索引 - min_index = np.argmin(centers) - i = min_index // num_y - j = min_index % num_y - - best_angle_x = angle_x_start + i * angle_step - best_angle_y = angle_y_start + j * angle_step - min_center = centers[i, j] - - return best_angle_x, best_angle_y, 0, min_center - -def read_mesh(obj_path, simple=True): - mesh_obj = o3d.io.read_triangle_mesh(obj_path) - return mesh_obj - -def compute_mesh_center2(vertices): - """ - 计算网格质心 - - 参数: - vertices: 顶点坐标数组,形状为(N, 3)的NumPy数组或列表 - - 返回: - centroid: 质心坐标的NumPy数组 [x, y, z] - """ - if len(vertices) == 0: - raise ValueError("顶点数组不能为空") - - n = len(vertices) # 顶点数量 - # 初始化坐标累加器 - sum_x, sum_y, sum_z = 0.0, 0.0, 0.0 - - start_time1 = time.time() - # 遍历所有顶点累加坐标值 - for vertex in vertices: - sum_x += vertex[0] - sum_y += vertex[1] - sum_z += vertex[2] - print("compute_mesh_center1 time", time.time()-start_time1) - - # 计算各坐标轴的平均值 - centroid = np.array([sum_x / n, sum_y / n, sum_z / n]) - return centroid - -def compute_mesh_center(vertices): - """ - 计算网格质心(优化版) - - 参数: - vertices: 顶点坐标数组,形状为(N, 3)的NumPy数组或列表 - - 返回: - centroid: 质心坐标的NumPy数组 [x, y, z] - """ - if len(vertices) == 0: - raise ValueError("顶点数组不能为空") - - # 确保vertices是NumPy数组 - vertices_np = np.asarray(vertices) - - # 使用NumPy的mean函数直接计算均值(向量化操作) - centroid = np.mean(vertices_np, axis=0) - - return centroid - -def get_lowest_position_of_center_ext3(mesh_obj, obj_path, voxel_size = 3): - - best_angle_x, best_angle_y, best_angle_z, z_mean_min, pcd_transformed= get_lowest_position_of_center2(mesh_obj, obj_path, voxel_size) - - return best_angle_x, best_angle_y, best_angle_z, z_mean_min - - -def get_lowest_position_of_center_ext2(mesh_obj, obj_path, total_matrix, voxel_size): - - # total_matrix, z_mean_min = get_lowest_position_of_center(obj_path, total_matrix, voxel_size) - temp_matrix, z_mean_min = get_lowest_position_of_center(mesh_obj, obj_path, np.eye(4), voxel_size) - # print("temp_matrix=",temp_matrix,voxel_size,mesh_obj) - - total_matrix = temp_matrix @ total_matrix - - return total_matrix, z_mean_min - -def get_lowest_position_of_center_ext(obj_path, total_matrix): - - temp_matrix, z_max = get_lowest_position_of_z_out(obj_path) - - total_matrix = temp_matrix @ total_matrix - - return total_matrix, z_max - -def down_sample(pcd, voxel_size, farthest_sample = False): - original_num = len(pcd.points) - target_samples = 1500 # 1000 - num_samples = min(target_samples, original_num) - - # 第一步:使用体素下采样快速减少点数量 - # voxel_size = 3 - if farthest_sample: - pcd_voxel = pcd.farthest_point_down_sample(num_samples=num_samples) - else: - pcd_voxel = pcd.voxel_down_sample(voxel_size) - down_num = len(pcd_voxel.points) - # print(f"original_num={original_num}, down_num={down_num}") - - # 第二步:仅在必要时进行最远点下采样 - if len(pcd_voxel.points) > target_samples and False: - pcd_downsampled = pcd_voxel.farthest_point_down_sample(num_samples=num_samples) - else: - pcd_downsampled = pcd_voxel - - return pcd_downsampled - -def get_lowest_position_of_center(mesh_obj, obj_path, total_matrix, voxel_size): - - # print(f"obj_path={obj_path}, get_lowest_position_of_center voxel_size={voxel_size}") - start_time1 = time.time() - - vertices = np.asarray(mesh_obj.vertices) - - # 确保网格有顶点 - if len(vertices) == 0: - # raise ValueError(f"Mesh has no vertices: {obj_path}") - print(f"Warning: Mesh has no vertices: {mesh_obj}") - return None - - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(vertices) - - # print("voxel_size",voxel_size,obj_path, len(pcd.points), len(mesh_obj.vertices)) - - # 对点云进行下采样(体素网格法) - #""" - pcd_downsampled = down_sample(pcd, voxel_size) - pcd_downsampled.paint_uniform_color([0, 0, 1]) - - if len(np.asarray(pcd_downsampled.points)) <= 0: - bbox = pcd.get_axis_aligned_bounding_box() - volume = bbox.volume() - - # print(f"len(pcd.points)={len(pcd.points)}, volume={volume}") - - # 处理体积为零的情况 - if volume <= 0: - # 计算点云的实际范围 - points = np.asarray(pcd.points) - if len(points) > 0: - min_bound = np.min(points, axis=0) - max_bound = np.max(points, axis=0) - extent = max_bound - min_bound - - # 确保最小维度至少为0.01 - min_dimension = max(0.01, np.min(extent)) - volume = min_dimension ** 3 - else: - volume = 1.0 # 最后的安全回退 - - print(f"Warning: Zero volume detected, using approximated volume {volume:.6f} for {obj_path}") - - # 安全计算密度 - 防止除零错误 - if len(pcd.points) > 0 and volume > 0: - original_density = len(pcd.points) / volume - voxel_size = max(0.01, min(10.0, 0.5 / (max(1e-6, original_density) ** 0.33))) - else: - # 当点数为0或体积为0时使用默认体素大小 - voxel_size = 1.0 # 默认值 - - print(f"Recalculated voxel_size: {voxel_size} for {obj_path}") - - pcd_downsampled = down_sample(pcd, voxel_size) - pcd_downsampled.paint_uniform_color([0, 0, 1]) - - original_num = len(pcd.points) - target_samples = 1000 - num_samples = min(target_samples, original_num) - - # print("get_lowest_position_of_center1 time", time.time()-start_time1) - start_time2 = time.time() - # 确保下采样后有点云 - if len(np.asarray(pcd_downsampled.points)) == 0: - # 使用原始点云作为后备 - pcd_downsampled = pcd - print(f"Warning: Using original point cloud for {obj_path} as downsampling produced no points") - - points = np.asarray(pcd_downsampled.points) - - # 初始化最小重心Y的值 - min_center_of_mass_y = float('inf') - best_angle_x, best_angle_y, best_angle_z = 0, 0, 0 - best_angle_x, best_angle_y, best_angle_z, min_center_of_mass_y = parallel_rotation4(points, angle_step=3) - - # 使用最佳角度进行旋转并平移obj - pcd_transformed = copy.deepcopy(mesh_obj) - - - # 最佳角度旋转 - R_x = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([1, 0, 0]) * np.radians(best_angle_x)) - pcd_transformed.rotate(R_x) - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * np.radians(best_angle_y)) - pcd_transformed.rotate(R_y) - R_z = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 0, 1]) * np.radians(best_angle_z)) - pcd_transformed.rotate(R_z) - - T_x = np.eye(4) - T_x[:3, :3] = R_x - center_point = compute_mesh_center(mesh_obj.vertices) - T_center_to_origin = np.eye(4) - T_center_to_origin[:3, 3] = -center_point - T_origin_to_center = np.eye(4) - T_origin_to_center[:3, 3] = center_point - T_rot_center = T_origin_to_center @ T_x @ T_center_to_origin - total_matrix = T_rot_center @ total_matrix - - T_y = np.eye(4) - T_y[:3, :3] = R_y - center_point = compute_mesh_center(mesh_obj.vertices) - T_center_to_origin = np.eye(4) - T_center_to_origin[:3, 3] = -center_point - T_origin_to_center = np.eye(4) - T_origin_to_center[:3, 3] = center_point - T_rot_center = T_origin_to_center @ T_y @ T_center_to_origin - total_matrix = T_rot_center @ total_matrix - - T_z = np.eye(4) - T_z[:3, :3] = R_z - center_point = compute_mesh_center(mesh_obj.vertices) - T_center_to_origin = np.eye(4) - T_center_to_origin[:3, 3] = -center_point - T_origin_to_center = np.eye(4) - T_origin_to_center[:3, 3] = center_point - T_rot_center = T_origin_to_center @ T_z @ T_center_to_origin - total_matrix = T_rot_center @ total_matrix - - #试着旋转180,让脸朝上 - - vertices = np.asarray(pcd_transformed.vertices) - # 计算平移向量,将最小Y值平移到0 - min_z = np.min(vertices[:, 2]) - translation_vector = np.array([0,0,-min_z,]) - pcd_transformed.translate(translation_vector) - - T_trans1 = np.eye(4) - T_trans1[:3, 3] = translation_vector - total_matrix = T_trans1 @ total_matrix - - # 计算 z 坐标均值 - vertices = np.asarray(pcd_transformed.vertices) - z_mean1 = np.mean(vertices[:, 2]) - z_max1 = np.max(vertices[:, 2]) - - angle_rad = np.pi - #print("旋转前质心:", pcd_transformed.get_center()) - #print("旋转前点示例:", np.asarray(pcd_transformed.vertices)[:3]) - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * angle_rad) - centroid = pcd_transformed.get_center() - pcd_transformed.translate(-center_point) - pcd_transformed.rotate(R_y, center=(0, 0, 0)) - pcd_transformed.translate(center_point) - - aabb = pcd_transformed.get_axis_aligned_bounding_box() - # center_point = aabb.get_center() - center_point = compute_mesh_center(mesh_obj.vertices) - # 构建绕中心点旋转的变换矩阵[3](@ref) - T_center_to_origin = np.eye(4) - T_center_to_origin[:3, 3] = -center_point - R_y180 = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * angle_rad) - T_rotate = np.eye(4) - T_rotate[:3, :3] = R_y180 - T_origin_to_center = np.eye(4) - T_origin_to_center[:3, 3] = center_point - T_rot_center = T_origin_to_center @ T_rotate @ T_center_to_origin - total_matrix = T_rot_center @ total_matrix - - #print("旋转后质心:", pcd_transformed.get_center()) - #print("旋转后点示例:", np.asarray(pcd_transformed.vertices)[:3]) - - # - vertices = np.asarray(pcd_transformed.vertices) - # 计算平移向量,将最小Y值平移到0 - min_z = np.min(vertices[:, 2]) - max_z = np.max(vertices[:, 2]) - # print("min_z1", min_z, obj_path) - translation_vector = np.array([0,0,-min_z,]) - # translation_vector = np.array([0,0,-min_z + (min_z-max_z),]) - # print("translation_vector1",translation_vector) - pcd_transformed.translate(translation_vector) - - T_trans2 = np.eye(4) - T_trans2[:3, 3] = translation_vector - translation = total_matrix[:3, 3] - # print("translation_vector2",translation_vector) - # print(1,translation) - - total_matrix = T_trans2 @ total_matrix - translation = total_matrix[:3, 3] - # print(2,translation) - - # 计算 z 坐标均值 - vertices = np.asarray(pcd_transformed.vertices) - z_mean2 = np.mean(vertices[:, 2]) - z_max2 = np.max(vertices[:, 2]) - - # print(f"get_lowest_position_of_center z_max1={z_max1}, z_max2={z_max2}, len={len(pcd_transformed.vertices)}, obj_path={obj_path}") - - if (z_mean2 > z_mean1): - # if (z_max2 > z_max1): - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * -angle_rad) - centroid = pcd_transformed.get_center() - - aabb = pcd_transformed.get_axis_aligned_bounding_box() - # center_point = aabb.get_center() - center_point = compute_mesh_center(mesh_obj.vertices) - - pcd_transformed.translate(-center_point) - pcd_transformed.rotate(R_y, center=(0, 0, 0)) - pcd_transformed.translate(center_point) - - T_center_to_origin = np.eye(4) - T_center_to_origin[:3, 3] = -center_point - T_origin_to_center = np.eye(4) - T_origin_to_center[:3, 3] = center_point - # 构建反向旋转矩阵 - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * -angle_rad) - T_rotate_inv = np.eye(4) - T_rotate_inv[:3, :3] = R_y - # 完整的反向绕中心旋转矩阵 - T_rot_center_inv = T_origin_to_center @ T_rotate_inv @ T_center_to_origin - total_matrix = T_rot_center_inv @ total_matrix - - vertices = np.asarray(pcd_transformed.vertices) - # 计算平移向量,将最小Y值平移到0 - min_z = np.min(vertices[:, 2]) - # print("min_z2", min_z, obj_path) - translation_vector = np.array([0,0,-min_z,]) - pcd_transformed.translate(translation_vector) - - T_trans3 = np.eye(4) - T_trans3[:3, 3] = translation_vector - total_matrix = T_trans3 @ total_matrix - - # z_mean_min = min(z_mean1, z_mean2) - z_max_min = min(z_max1, z_max2) - - # print("get_lowest_position_of_center2 time", time.time()-start_time2) - return total_matrix, z_max_min - -import requests -import json -import re - -def is_valid_float_string(s): - # 匹配科学计数法或普通小数,允许开头和末尾的空格 - pattern = r'^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$' - return bool(re.match(pattern, s.strip())) - -def safe_convert_to_float(s): - """ - 尝试将字符串安全转换为float,处理一些不完整情况。 - """ - s = s.strip().lower() # 去除空格,统一为小写 - - # 处理空字符串或完全非数字的情况 - if not s or s in ['na', 'nan', 'inf', 'null', 'none']: - return None # 或者可以根据需要返回 float('nan') - - # 检查是否为有效的浮点数字符串 - if is_valid_float_string(s): - return float(s) - - # 处理类似 '0.00000000e' 的情况(缺少指数) - if s.endswith('e'): - # 尝试添加指数 '0' -> 'e0' - try: - return float(s + '0') - except ValueError: - pass # 如果添加'e0'后仍然失败,则继续下面的异常处理 - - # 更激进的清理:移除非数字、小数点、负号和指数e以外的所有字符 - # 注意:这可能破坏有特定格式的字符串,慎用 - cleaned_s = re.sub(r'[^\d\.eE-]', '', s) - try: - return float(cleaned_s) - except ValueError: - pass - - # 如果所有尝试都失败,返回None或抛出异常 - return None - -def string_to_matrix(data_string): - """ - 将字符串转换为NumPy浮点数矩阵,并处理可能的转换错误。 - """ - # 分割字符串 - lines = data_string.strip().split(';') - matrix = [] - - for line in lines: - num_list = line.split() - float_row = [] - for num in num_list: - # 使用安全转换函数 - value = safe_convert_to_float(num) - if value is None: - # 处理转换失败,例如记录日志、使用NaN代替 - print(f"警告: 无法转换字符串 '{num}',将其替换为NaN。") - value = np.nan # 用NaN标记缺失或无效值 - float_row.append(value) - matrix.append(float_row) - - return np.array(matrix) - -import ast - -def get_lowest_position_of_center_net(printId, total_matrix): - print("get_lowest_position_of_center_net", printId) - - url = f"https://mp.api.suwa3d.com/api/printOrder/infoByPrintId?printId={printId}" - res = requests.get(url) - - datas = res.json()["data"]["layout"] - print("datas=", datas) - - homo_matrix_str = datas.get("homo_matrix") - print("homo_matrix_str=", homo_matrix_str) - - # 1. 去除字符串首尾的方括号 - str_cleaned = homo_matrix_str.strip('[]') - # 2. 按行分割字符串 - rows = str_cleaned.split('\n') - - # 3. 修复:处理每行中的逗号问题 - matrix_list = [] - for row in rows: - if row.strip() == '': - continue - - # 去除行首尾的方括号和空格 - row_cleaned = row.strip(' []') - # 按逗号分割,但过滤掉空字符串 - elements = [elem.strip() for elem in row_cleaned.split(',') if elem.strip() != ''] - - # 进一步清理每个元素:去除可能残留的逗号和方括号 - cleaned_elements = [] - for elem in elements: - # 去除元素中可能存在的逗号、方括号和空格 - elem_cleaned = elem.strip(' ,[]') - if elem_cleaned != '': - cleaned_elements.append(elem_cleaned) - - if cleaned_elements: # 只添加非空行 - matrix_list.append(cleaned_elements) - - print("matrix_list=", matrix_list) - - # 4. 安全地转换为浮点数数组(带错误处理) - try: - reconstructed_matrix = np.array(matrix_list, dtype=float) - except ValueError as e: - print(f"转换矩阵时出错: {e}") - print("尝试逐个元素转换...") - - # 逐个元素转换,便于定位问题元素 - float_matrix = [] - for i, row in enumerate(matrix_list): - float_row = [] - for j, elem in enumerate(row): - try: - # 再次清理元素并转换 - cleaned_elem = elem.strip(' ,') - float_val = float(cleaned_elem) - float_row.append(float_val) - except ValueError as ve: - print(f"无法转换的元素: 行{i}, 列{j}, 值'{elem}', 错误: {ve}") - # 可以选择设置为0或NaN,或者抛出异常 - float_row.append(0.0) # 或者 np.nan - float_matrix.append(float_row) - - reconstructed_matrix = np.array(float_matrix, dtype=float) - - layout_z = datas.get("layout_z", 0) - print("layout_z", layout_z) - - reconstructed_matrix = reconstructed_matrix @ total_matrix - print("reconstructed_matrix=", reconstructed_matrix) - - return reconstructed_matrix, layout_z - -def get_lowest_position_of_center2(mesh_obj, obj_path, voxel_size = 3): - - # mesh_obj = read_mesh(obj_path) - - vertices = np.asarray(mesh_obj.vertices) - - # 确保网格有顶点 - if len(vertices) == 0: - print(f"Warning: Mesh has no vertices: {obj_path}") - return None - - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(vertices) - - # 对点云进行下采样(体素网格法 - pcd_downsampled = down_sample(pcd, voxel_size) - pcd_downsampled.paint_uniform_color([0, 0, 1]) - - if len(np.asarray(pcd_downsampled.points)) <= 0: - bbox = pcd.get_axis_aligned_bounding_box() - volume = bbox.volume() - - # print(f"len(pcd.points)={len(pcd.points)}, volume={volume}") - - # 处理体积为零的情况 - if volume <= 0: - # 计算点云的实际范围 - points = np.asarray(pcd.points) - if len(points) > 0: - min_bound = np.min(points, axis=0) - max_bound = np.max(points, axis=0) - extent = max_bound - min_bound - - # 确保最小维度至少为0.01 - min_dimension = max(0.01, np.min(extent)) - volume = min_dimension ** 3 - else: - volume = 1.0 # 最后的安全回退 - - print(f"Warning: Zero volume detected, using approximated volume {volume:.6f} for {obj_path}") - - # 安全计算密度 - 防止除零错误 - if len(pcd.points) > 0 and volume > 0: - original_density = len(pcd.points) / volume - voxel_size = max(0.01, min(10.0, 0.5 / (max(1e-6, original_density) ** 0.33))) - else: - # 当点数为0或体积为0时使用默认体素大小 - voxel_size = 1.0 # 默认值 - - print(f"Recalculated voxel_size: {voxel_size} for {obj_path}") - - pcd_downsampled = down_sample(pcd, voxel_size) - pcd_downsampled.paint_uniform_color([0, 0, 1]) - - # 确保下采样后有点云 - if len(np.asarray(pcd_downsampled.points)) == 0: - # 使用原始点云作为后备 - pcd_downsampled = pcd - print(f"Warning: Using original point cloud for {obj_path} as downsampling produced no points") - - points = np.asarray(pcd_downsampled.points) - - # 初始化最小重心Y的值 - best_angle_x, best_angle_y, best_angle_z = 0, 0, 0 - # 旋转并计算最优角度:绕X、Y、Z轴进行每度的旋转 - best_angle_x, best_angle_y, best_angle_z, min_center_of_mass_y = parallel_rotation4(points, angle_step=3) - # print("best_angle1", best_angle_x, best_angle_y, best_angle_z) - - # 使用最佳角度进行旋转并平移obj - pcd_transformed = copy.deepcopy(mesh_obj) - - # 最佳角度旋转 - """ - R_x = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([1, 0, 0]) * np.radians(best_angle_x)) - pcd_transformed.rotate(R_x) - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * np.radians(best_angle_y)) - pcd_transformed.rotate(R_y) - R_z = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 0, 1]) * np.radians(best_angle_z)) - pcd_transformed.rotate(R_z) - #""" - - aabb = pcd_transformed.get_axis_aligned_bounding_box() - center_point = aabb.get_center() - - vertices = np.asarray(pcd_transformed.vertices) - # 计算平移向量,将最小Y值平移到0 - min_z = np.min(vertices[:, 2]) - translation_vector = np.array([0,0,-min_z,]) - pcd_transformed.translate(translation_vector) - - # 计算 z 坐标均值 - vertices = np.asarray(pcd_transformed.vertices) - z_mean1 = np.mean(vertices[:, 2]) - - angle_rad = np.pi - - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * angle_rad) - - pcd_transformed.translate(-center_point) - pcd_transformed.rotate(R_y, center=(0, 0, 0)) - pcd_transformed.translate(center_point) - best_angle_y += angle_rad / np.pi * 180 - - vertices = np.asarray(pcd_transformed.vertices) - # 计算平移向量,将最小Y值平移到0 - min_z = np.min(vertices[:, 2]) - translation_vector = np.array([0,0,-min_z,]) - pcd_transformed.translate(translation_vector) - - # 计算 z 坐标均值 - vertices = np.asarray(pcd_transformed.vertices) - z_mean2 = np.mean(vertices[:, 2]) - - # print("z_mean",z_mean1,z_mean2,len(pcd_transformed.vertices),obj_path) - - if z_mean2 > z_mean1: - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * -angle_rad) - - aabb = pcd_transformed.get_axis_aligned_bounding_box() - center_point = aabb.get_center() - - pcd_transformed.translate(-center_point) - pcd_transformed.rotate(R_y, center=(0, 0, 0)) - pcd_transformed.translate(center_point) - best_angle_y += -angle_rad / np.pi * 180 - - vertices = np.asarray(pcd_transformed.vertices) - # 计算平移向量,将最小Y值平移到0 - min_z = np.min(vertices[:, 2]) - translation_vector = np.array([0,0,-min_z,]) - pcd_transformed.translate(translation_vector) - - z_mean_min = min(z_mean1, z_mean2) - - angle_z_delta = arrange_box_correctly(pcd_transformed, voxel_size) - best_angle_z += angle_z_delta - # print("angle_z_delta", angle_z_delta, best_angle_z) - - return best_angle_x, best_angle_y, best_angle_z, z_mean_min, pcd_transformed - -def arrange_box_correctly(obj_transformed, voxel_size): - - vertices = np.asarray(obj_transformed.vertices) - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(vertices) - - # 降采样与特征计算 - pcd_downsampled = down_sample(pcd, voxel_size) - - original_num = len(pcd.points) - target_samples = 1000 - num_samples = min(target_samples, original_num) - - points = np.asarray(pcd_downsampled.points) - cov = np.cov(points.T) - - center = obj_transformed.get_center() - - # 特征分解与方向约束(关键修改点) - eigen_vals, eigen_vecs = np.linalg.eigh(cov) - max_axis = eigen_vecs[:, np.argmax(eigen_vals)] - - # 强制主方向向量X分量为正(指向右侧) - if max_axis[0] < 0 or (max_axis[0] == 0 and max_axis[1] < 0): - max_axis = -max_axis - - target_dir = np.array([1, 0]) # 目标方向为X正轴 - current_dir = max_axis[:2] / np.linalg.norm(max_axis[:2]) - dot_product = np.dot(current_dir, target_dir) - - # print("dot_product", dot_product) - if dot_product < 0.8: # 阈值控制方向敏感性(建议0.6~0.9) - max_axis = -max_axis # 强制翻转方向 - - # 计算旋转角度 - angle_z = np.arctan2(max_axis[1], max_axis[0]) % (2 * np.pi) - - if max_axis[0] <= 0 and max_axis[1] <= 0: - angle_z += np.pi - - # print("max_axis2", max_axis, -angle_z, np.rad2deg(-angle_z)) - - R = o3d.geometry.get_rotation_matrix_from_axis_angle([0, 0, -angle_z]) - - T = np.eye(4) - T[:3, :3] = R - T[:3, 3] = center - R.dot(center) # 保持中心不变 - obj_transformed.transform(T) - - return np.rad2deg(-angle_z) diff --git a/get_lowest_position_of_z_out.py b/get_lowest_position_of_z_out.py deleted file mode 100644 index 3de702f..0000000 --- a/get_lowest_position_of_z_out.py +++ /dev/null @@ -1,352 +0,0 @@ -import open3d as o3d -import numpy as np -import copy -import time -import argparse - -""" -对外部提供的获取最低z的接口 -get_lowest_position_of_z_out - -参数: - obj_path, 模型数据路径 - -返回: - total_matrix: 旋转矩阵 - z_max: Z最高点 -""" - -def get_lowest_position_of_z_out(obj_path): - - mesh_obj = o3d.io.read_triangle_mesh(obj_path) - - total_matrix = np.eye(4) - - voxel_size = 3 - - # print(f"obj_path={obj_path}, get_lowest_position_of_center voxel_size={voxel_size}") - start_time1 = time.time() - - vertices = np.asarray(mesh_obj.vertices) - - # 确保网格有顶点 - if len(vertices) == 0: - # raise ValueError(f"Mesh has no vertices: {obj_path}") - print(f"Warning: Mesh has no vertices: {mesh_obj}") - return None - - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(vertices) - - # print("voxel_size",voxel_size,obj_path, len(pcd.points), len(mesh_obj.vertices)) - - # 对点云进行下采样(体素网格法) - #""" - pcd_downsampled = down_sample(pcd, voxel_size) - pcd_downsampled.paint_uniform_color([0, 0, 1]) - - if len(np.asarray(pcd_downsampled.points)) <= 0: - bbox = pcd.get_axis_aligned_bounding_box() - volume = bbox.volume() - - # print(f"len(pcd.points)={len(pcd.points)}, volume={volume}") - - # 处理体积为零的情况 - if volume <= 0: - # 计算点云的实际范围 - points = np.asarray(pcd.points) - if len(points) > 0: - min_bound = np.min(points, axis=0) - max_bound = np.max(points, axis=0) - extent = max_bound - min_bound - - # 确保最小维度至少为0.01 - min_dimension = max(0.01, np.min(extent)) - volume = min_dimension ** 3 - else: - volume = 1.0 # 最后的安全回退 - - print(f"Warning: Zero volume detected, using approximated volume {volume:.6f} for {obj_path}") - - # 安全计算密度 - 防止除零错误 - if len(pcd.points) > 0 and volume > 0: - original_density = len(pcd.points) / volume - voxel_size = max(0.01, min(10.0, 0.5 / (max(1e-6, original_density) ** 0.33))) - else: - # 当点数为0或体积为0时使用默认体素大小 - voxel_size = 1.0 # 默认值 - - print(f"Recalculated voxel_size: {voxel_size} for {obj_path}") - - pcd_downsampled = down_sample(pcd, voxel_size) - pcd_downsampled.paint_uniform_color([0, 0, 1]) - - original_num = len(pcd.points) - target_samples = 1000 - num_samples = min(target_samples, original_num) - - # print("get_lowest_position_of_center1 time", time.time()-start_time1) - start_time2 = time.time() - # 确保下采样后有点云 - if len(np.asarray(pcd_downsampled.points)) == 0: - # 使用原始点云作为后备 - pcd_downsampled = pcd - print(f"Warning: Using original point cloud for {obj_path} as downsampling produced no points") - - points = np.asarray(pcd_downsampled.points) - - # 初始化最小重心Y的值 - max_z_of_mass_y = float('inf') - best_angle_x, best_angle_y, best_angle_z = 0, 0, 0 - best_angle_x, best_angle_y, best_angle_z, max_z_of_mass_y = parallel_rotation(points, angle_step=3) - - # 使用最佳角度进行旋转并平移obj - pcd_transformed = copy.deepcopy(mesh_obj) - - # 最佳角度旋转 - R_x = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([1, 0, 0]) * np.radians(best_angle_x)) - pcd_transformed.rotate(R_x) - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * np.radians(best_angle_y)) - pcd_transformed.rotate(R_y) - R_z = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 0, 1]) * np.radians(best_angle_z)) - pcd_transformed.rotate(R_z) - - T_x = np.eye(4) - T_x[:3, :3] = R_x - center_point = compute_mesh_center(mesh_obj.vertices) - T_center_to_origin = np.eye(4) - T_center_to_origin[:3, 3] = -center_point - T_origin_to_center = np.eye(4) - T_origin_to_center[:3, 3] = center_point - T_rot_center = T_origin_to_center @ T_x @ T_center_to_origin - total_matrix = T_rot_center @ total_matrix - - T_y = np.eye(4) - T_y[:3, :3] = R_y - center_point = compute_mesh_center(mesh_obj.vertices) - T_center_to_origin = np.eye(4) - T_center_to_origin[:3, 3] = -center_point - T_origin_to_center = np.eye(4) - T_origin_to_center[:3, 3] = center_point - T_rot_center = T_origin_to_center @ T_y @ T_center_to_origin - total_matrix = T_rot_center @ total_matrix - - T_z = np.eye(4) - T_z[:3, :3] = R_z - center_point = compute_mesh_center(mesh_obj.vertices) - T_center_to_origin = np.eye(4) - T_center_to_origin[:3, 3] = -center_point - T_origin_to_center = np.eye(4) - T_origin_to_center[:3, 3] = center_point - T_rot_center = T_origin_to_center @ T_z @ T_center_to_origin - total_matrix = T_rot_center @ total_matrix - - #试着旋转180,让脸朝上 - - vertices = np.asarray(pcd_transformed.vertices) - # 计算平移向量,将最小Y值平移到0 - min_z = np.min(vertices[:, 2]) - translation_vector = np.array([0,0,-min_z,]) - pcd_transformed.translate(translation_vector) - - T_trans1 = np.eye(4) - T_trans1[:3, 3] = translation_vector - total_matrix = T_trans1 @ total_matrix - - # 计算 z 坐标均值 - vertices = np.asarray(pcd_transformed.vertices) - z_mean1 = np.mean(vertices[:, 2]) - z_max1 = np.max(vertices[:, 2]) - - angle_rad = np.pi - #print("旋转前质心:", pcd_transformed.get_center()) - #print("旋转前点示例:", np.asarray(pcd_transformed.vertices)[:3]) - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * angle_rad) - centroid = pcd_transformed.get_center() - pcd_transformed.translate(-center_point) - pcd_transformed.rotate(R_y, center=(0, 0, 0)) - pcd_transformed.translate(center_point) - - aabb = pcd_transformed.get_axis_aligned_bounding_box() - # center_point = aabb.get_center() - center_point = compute_mesh_center(mesh_obj.vertices) - # 构建绕中心点旋转的变换矩阵[3](@ref) - T_center_to_origin = np.eye(4) - T_center_to_origin[:3, 3] = -center_point - R_y180 = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * angle_rad) - T_rotate = np.eye(4) - T_rotate[:3, :3] = R_y180 - T_origin_to_center = np.eye(4) - T_origin_to_center[:3, 3] = center_point - T_rot_center = T_origin_to_center @ T_rotate @ T_center_to_origin - total_matrix = T_rot_center @ total_matrix - - #print("旋转后质心:", pcd_transformed.get_center()) - #print("旋转后点示例:", np.asarray(pcd_transformed.vertices)[:3]) - - # - vertices = np.asarray(pcd_transformed.vertices) - # 计算平移向量,将最小Y值平移到0 - min_z = np.min(vertices[:, 2]) - max_z = np.max(vertices[:, 2]) - # print("min_z1", min_z, obj_path) - translation_vector = np.array([0,0,-min_z,]) - # translation_vector = np.array([0,0,-min_z + (min_z-max_z),]) - # print("translation_vector1",translation_vector) - pcd_transformed.translate(translation_vector) - - T_trans2 = np.eye(4) - T_trans2[:3, 3] = translation_vector - translation = total_matrix[:3, 3] - # print("translation_vector2",translation_vector) - # print(1,translation) - - total_matrix = T_trans2 @ total_matrix - translation = total_matrix[:3, 3] - # print(2,translation) - - # 计算 z 坐标均值 - vertices = np.asarray(pcd_transformed.vertices) - z_mean2 = np.mean(vertices[:, 2]) - z_max2 = np.max(vertices[:, 2]) - - # print(f"get_lowest_position_of_center z_max1={z_max1}, z_max2={z_max2}, len={len(pcd_transformed.vertices)}, obj_path={obj_path}") - - if (z_mean2 > z_mean1): - # if (z_max2 > z_max1): - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * -angle_rad) - centroid = pcd_transformed.get_center() - - aabb = pcd_transformed.get_axis_aligned_bounding_box() - # center_point = aabb.get_center() - center_point = compute_mesh_center(mesh_obj.vertices) - - pcd_transformed.translate(-center_point) - pcd_transformed.rotate(R_y, center=(0, 0, 0)) - pcd_transformed.translate(center_point) - - T_center_to_origin = np.eye(4) - T_center_to_origin[:3, 3] = -center_point - T_origin_to_center = np.eye(4) - T_origin_to_center[:3, 3] = center_point - # 构建反向旋转矩阵 - R_y = pcd_transformed.get_rotation_matrix_from_axis_angle(np.array([0, 1, 0]) * -angle_rad) - T_rotate_inv = np.eye(4) - T_rotate_inv[:3, :3] = R_y - # 完整的反向绕中心旋转矩阵 - T_rot_center_inv = T_origin_to_center @ T_rotate_inv @ T_center_to_origin - total_matrix = T_rot_center_inv @ total_matrix - - vertices = np.asarray(pcd_transformed.vertices) - # 计算平移向量,将最小Y值平移到0 - min_z = np.min(vertices[:, 2]) - # print("min_z2", min_z, obj_path) - translation_vector = np.array([0,0,-min_z,]) - pcd_transformed.translate(translation_vector) - - T_trans3 = np.eye(4) - T_trans3[:3, 3] = translation_vector - total_matrix = T_trans3 @ total_matrix - - # z_mean_min = min(z_mean1, z_mean2) - z_max = min(z_max1, z_max2) - - # print("get_lowest_position_of_center2 time", time.time()-start_time2) - return total_matrix, z_max - -def calculate_rotation_and_top_of_mass(angle_x, angle_y, angle_z, points): - """计算某一组旋转角度后的重心""" - # 计算绕X轴、Y轴和Z轴的旋转矩阵 - R_x = np.array([ - [1, 0, 0], - [0, np.cos(np.radians(angle_x)), -np.sin(np.radians(angle_x))], - [0, np.sin(np.radians(angle_x)), np.cos(np.radians(angle_x))] - ]) - - R_y = np.array([ - [np.cos(np.radians(angle_y)), 0, np.sin(np.radians(angle_y))], - [0, 1, 0], - [-np.sin(np.radians(angle_y)), 0, np.cos(np.radians(angle_y))] - ]) - - R_z = np.array([ - [np.cos(np.radians(angle_z)), -np.sin(np.radians(angle_z)), 0], - [np.sin(np.radians(angle_z)), np.cos(np.radians(angle_z)), 0], - [0, 0, 1] - ]) - - # 综合旋转矩阵 - R = R_z @ R_y @ R_x - - # 执行旋转 - rotated_points = points @ R.T - - # 计算最小z值 - min_z = np.min(rotated_points[:, 2]) - - # 计算平移向量,将最小Z值平移到0 - translation_vector = np.array([0, 0, -min_z]) - rotated_points += translation_vector - - top_of_mass = np.max(rotated_points, axis=0) - - return top_of_mass[2], angle_x, angle_y, angle_z - -def parallel_rotation(points, angle_step=4): - """仅绕 Y 轴旋转(假设 X/Z 轴不影响目标函数)""" - max_top = float('inf') - - for angle_x in range(-90, 90, angle_step): - for angle_y in range(0, 360, angle_step): - max_z, ax, ay, _ = calculate_rotation_and_top_of_mass(angle_x, angle_y, 0, points) - if max_z < max_top: - max_top = max_z - best_angle_x = ax - best_angle_y = ay - - return (best_angle_x, best_angle_y, 0, max_top) - -def compute_mesh_center(vertices): - - if len(vertices) == 0: - raise ValueError("顶点数组不能为空") - - # 确保vertices是NumPy数组 - vertices_np = np.asarray(vertices) - - # 使用NumPy的mean函数直接计算均值(向量化操作) - centroid = np.mean(vertices_np, axis=0) - - return centroid - -def down_sample(pcd, voxel_size, farthest_sample = False): - original_num = len(pcd.points) - target_samples = 1500 # 1000 - num_samples = min(target_samples, original_num) - - # 第一步:使用体素下采样快速减少点数量 - # voxel_size = 3 - if farthest_sample: - pcd_voxel = pcd.farthest_point_down_sample(num_samples=num_samples) - else: - pcd_voxel = pcd.voxel_down_sample(voxel_size) - down_num = len(pcd_voxel.points) - # print(f"original_num={original_num}, down_num={down_num}") - - # 第二步:仅在必要时进行最远点下采样 - if len(pcd_voxel.points) > target_samples and False: - pcd_downsampled = pcd_voxel.farthest_point_down_sample(num_samples=num_samples) - else: - pcd_downsampled = pcd_voxel - - return pcd_downsampled - -if __name__ == '__main__': - - parser = argparse.ArgumentParser() - parser.add_argument("--obj_path", type=str, required=True, help="batchobj_path_id") - args = parser.parse_args() - - obj_path = args.obj_path - max, z = get_lowest_position_of_z_out(obj_path) - diff --git a/grid_near_three.py b/grid_near_three.py deleted file mode 100644 index 9838f30..0000000 --- a/grid_near_three.py +++ /dev/null @@ -1,179 +0,0 @@ -import os -import time - -import open3d as o3d -import numpy as np - - - -def make_near_dict(base_dir,compact_dir): - """""" - - # 用于存储结果的字典 - results = {} - # 遍历目录中的所有 .ply 文件 - for ply_file in os.listdir(base_dir): - # 检查文件是否为 .ply 格式 - if ply_file.endswith('.ply'): - ply_path = os.path.join(base_dir, ply_file) - compact_ply_path = os.path.join(compact_dir, ply_file) - if os.path.exists(compact_ply_path): - ply_read_path = compact_ply_path - else: - ply_read_path = ply_path - # 读取点云 - pcd = o3d.io.read_point_cloud(ply_read_path) - - # 获取点云的点数据 - points = np.asarray(pcd.points) - - # 计算质心 - centroid = np.mean(points, axis=0) - - # 计算 Y 轴最小值 - min_y_value = np.min(points[:, 1]) # Y 轴最小值 - max_y_value = np.max(points[:, 1]) - - # 计算 X 轴最小值 - min_x_value = np.min(points[:, 0]) # X 轴最小值 - max_x_value = np.max(points[:, 0]) # X 轴最小值 - #ply_pid = ply_file.split("_")[0] - # 将结果存入字典 - results[ply_file] = { - "centroid": centroid, - "min_x_value": min_x_value, - "min_y_value": min_y_value, - "max_x_value": max_x_value, - "max_y_value": max_y_value, - - } - - # 打印结果 - # for ply_file, values in results.items(): - # print(f"文件: {ply_file}") - # print(f" 质心: {values['centroid']}") - # print(f" X 轴最小值: {values['min_x_value']}") - # print(f" Y 轴最小值: {values['min_y_value']}") - - # 计算每个ply需要触碰检测的 - check_touch_dict = {} - for ply_file in results.keys(): - print(ply_file) - #ply_pid = ply_file.split("_")[0] - #print(ply_pid) - bounds_min_x = results[ply_file]["min_x_value"] - bounds_min_y = results[ply_file]["min_y_value"] - #bounds_center = results[ply_file]["centroid"] - need_check_list = [] - need_values_dict = {} - for ply_file_near in results.keys(): - #print(ply_file_near) - if ply_file!= ply_file_near: - bounds_max_x = results[ply_file_near]["max_x_value"] - bounds_max_y = results[ply_file_near]["max_y_value"] - # if ply_file == "151140_9cm_x1=30.578+41.705+90.753.ply": - # print("-"*50) - # print("主::",ply_file) - # print("从::",ply_file_near) - # print(f"center_x{bounds_center[0]}") - # print(f"center_y{bounds_center[1]}") - # print(f"bounds_max_x{bounds_max_x}") - # print(f"bounds_max_y{bounds_max_y}") - # time.sleep(3) - # 235605_12cm_x1=33.774+30.837+120.344.ply - # if bounds_center[0]bounds_min_x and bounds_near_center[1]>bounds_min_y: - #print("-"*50) - # print(f"bounds_min_x{bounds_min_x}") - # print(f"bounds_max_x{bounds_max_x}") - - x_dis = bounds_min_x - bounds_max_x - y_dis = bounds_min_y - bounds_max_y - #print(f"x_dis=={x_dis}") - #print(f"y_dis=={y_dis}") - #if ply_file=="158040_15cm_x1=80.682+89.345+152.468.ply": - #if ply_file == "235547_4.8cm_x1=29.339+39.528+57.63.ply": - # print("主::",ply_file) - # print("从::",ply_file_near) - #if ply_file == "158040_15cm_x1=80.682+89.345+152.468.ply": - #if ply_file == "151140_9cm_x1=30.578+41.705+90.753.ply": - # print("主::", ply_file) - # print("临近::", ply_file_near) - # time.sleep(3) - if x_dis<-10 and y_dis<-10: - need_check_list.append(ply_file_near) - need_values_dict["need_check_list"] = need_check_list - # need_values_dict["max_x_value"] = bounds_max_x - # need_values_dict["max_y_value"] = bounds_max_y - - check_touch_dict[ply_file] = need_values_dict - - - # print(check_touch_dict) - # print("开始要计算触碰检测的数据") - # for ply_file, values in check_touch_dict.items(): - # print("*"*50) - # print(ply_file) - # print(values) - - # 去掉离比较远的数据 - for check_touch_key,check_touch_values in check_touch_dict.items(): - print("-"*50) - #print(check_touch_key) - #print(check_touch_values["need_check_list"]) - need_check_list= check_touch_values["need_check_list"] - #print(len(need_check_list)) - if len(need_check_list)>2: - ply_A_path = os.path.join(base_dir, check_touch_key) - compact_ply_path = os.path.join(compact_dir, check_touch_key) - if os.path.exists(compact_ply_path): - ply_read_path = compact_ply_path - else: - ply_read_path = ply_A_path - pcd_A = o3d.io.read_point_cloud(ply_read_path) - points_A = np.asarray(pcd_A.points) - distances = [] - for i, check_touch in enumerate(need_check_list): - point = results[check_touch]['centroid'] - ply_path = os.path.join(base_dir, check_touch) - - # 读取当前点云 - pcd = o3d.io.read_point_cloud(ply_path) - points = np.asarray(pcd.points) - - # 计算点云之间最小点对距离(brute-force) - diff = points_A[:, np.newaxis, :] - points[np.newaxis, :, :] # (N, M, 3) - dists = np.linalg.norm(diff, axis=2) # (N, M) - min_distance = np.min(dists) - - #print(f"check_touch: {check_touch}, centroid: {point}, min_distance: {min_distance:.4f}") - distances.append((i, point, min_distance, check_touch)) - distances.sort(key=lambda x: x[2]) - - # 提取最近的 3 个点 - nearest_points = distances[:5] - last_elements = [item[-1] for item in nearest_points] - # print(f"nearest_points---------{nearest_points}") - # print(f"check_touch_key--------{check_touch_key}") - # print(f"last_elements--------{last_elements}") - check_touch_dict[check_touch_key]["need_check_list"] = last_elements - - return check_touch_dict - - # for check_touch_key,check_touch_values in check_touch_dict.items(): - # print("*"*50) - # print(check_touch_key) - # print(check_touch_values) - - -if __name__ == '__main__': - bounds_fix_out_dir = "/data/datasets_20t/type_setting_test_data/print_bounds_fix_data/" - check_touch_dict=make_near_dict(bounds_fix_out_dir) - print(f"check_touch_dict--------------{check_touch_dict}") - """ - {'need_check_list': ['131508_18cm_x1=51.412+87.921+181.446.ply', '239617_12cm_x1=43.987+54.233+120.691.ply']}, - """ - diff --git a/output.log b/output.log new file mode 100644 index 0000000..4d372c9 --- /dev/null +++ b/output.log @@ -0,0 +1,295 @@ +is_run_local_data=True +pre_batch_id=991112 +print_ids=[85240, 88136] +---------------------------------------- +---------------------------------------- +路径 /home/algo/Documents/print_factory_type/data/ 不是一个有效目录。 +目录 /home/algo/Documents/print_factory_type/full/ 下的内容已清空。 +从文件读取了 2 个path +开始下载print_model_info: BatchModelInfo(order_id=875986, pid=305425, print_order_id=88136, model_size='9cm_x1', path='objs/download/print/305425/base/model/9cm', count=1) +target_dir= /home/algo/Documents/print_factory_type/data/991112 +前缀 'objs/download/print/305425/base/model/9cm/' 存在,共找到 6 个对象。 +已更新材质库引用: 305425Tex1.jpg -> 875986_305425Tex1.jpg +下载文件: objs/download/print/305425/base/model/9cm/305425.mtl -> /home/algo/Documents/print_factory_type/data/991112/875986_305425.mtl +下载文件: objs/download/print/305425/base/model/9cm/305425.obj -> /home/algo/Documents/print_factory_type/data/991112/875986_305425_P88136_9cm_x1.obj +下载文件: objs/download/print/305425/base/model/9cm/305425Tex1.jpg -> /home/algo/Documents/print_factory_type/data/991112/875986_305425Tex1.jpg +下载文件: objs/download/print/305425/base/model/9cm/blend/305425_org.blend -> /home/algo/Documents/print_factory_type/data/991112/blend/305425_org.blend +下载 in obj_key +下载 in obj_key +开始下载print_model_info: BatchModelInfo(order_id=857420, pid=268473, print_order_id=85240, model_size='5cm_x1', path='objs/download/print/268473/base_cartoon/badge/1/5cm', count=1) +target_dir= /home/algo/Documents/print_factory_type/data/991112 +前缀 'objs/download/print/268473/base_cartoon/badge/1/5cm/' 存在,共找到 4 个对象。 +已更新材质库引用: 268473Tex1.jpg -> 857420_268473Tex1.jpg +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473.mtl -> /home/algo/Documents/print_factory_type/data/991112/857420_268473.mtl +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473.obj -> /home/algo/Documents/print_factory_type/data/991112/857420_268473_P85240_5cm_x1.obj +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473Tex1.jpg -> /home/algo/Documents/print_factory_type/data/991112/857420_268473Tex1.jpg +下载 in obj_key +下载耗时:5.1790547370910645 +selected_machine 小机型 selected_mode 紧凑 output_format JSON +make_bbox_for_print total_time=5.503540992736816 +z_volume_center1=3.494785873362541, z_volume_center2=4.630428442012234 +compute_bbox 857420_268473_P85240_5cm_x1.obj time=2.4211812019348145 +z_volume_center1=7.4748487042688625, z_volume_center2=8.653334486695568 +compute_bbox 875986_305425_P88136_9cm_x1.obj time=2.196851968765259 +all_models [{'name': '857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply', 'dimensions': (50, 50, 9)}, {'name': '875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply', 'dimensions': (90, 25, 19)}] +开始计算排序... +开始计算排序... +⚠️ 单层放置模式:所有模型只能放在平台底面(Z=0) +arrange_models 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply +pre_name1=875986_305425_P88136_9cm, pre_name2= +First Model 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply +model position1 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply (96, 339, 0) +arrange_models 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply +pre_name1=857420_268473_P85240_5cm, pre_name2= +857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply px 150 96 90 +final_y2 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply 56 345 50 6 150 +can_place False 1 cross_border 150, 340, 0, {'name': '857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply', 'dimensions': (50, 50, 9), 'first_line': True}, 380, 345, 250 +model position2 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply (150, 339, 0) +Placed Models: + - 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply at (96, 339, 0) with dimensions (90, 25, 19) + - 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply at (150, 339, 0) with dimensions (50, 50, 9) +Unplaced Models: + - 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply at (96, 339, 0) with dimensions (90, 25, 19) + - 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply at (150, 339, 0) with dimensions (50, 50, 9) +pcd_second : len(pcd_first)=2, len(pcd_second)=0, len(last_pcd_processed)=2, len(pcd_all)=2 +is_test= True +need_upload_result=False +3D打印布局已保存至: /home/algo/Documents/print_factory_type/data/991112/991112.json +排版错误模型数量::0 +排版剩余模型数量::0 +排版完成 +排版总耗时::0 分 / 16.58465886116028 秒 +计算重心::0 分 / 16.583354234695435 秒 +排包围盒::0 分 / 0.0005736351013183594 秒 +挪紧凑::0 分 / 8.440017700195312e-05 秒 +移动到位置::0 分 / 0.0006456375122070312 秒 +选择机型=小机型 +已加载并变换: 857420_268473_P85240_5cm_x1.obj +已加载并变换: 875986_305425_P88136_9cm_x1.obj +add_plank /home/algo/Documents/print_factory_type/blank/blank_bias/blank_small.obj +成功加载并变换了 3 个模型 +高级渲染图片已保存到: /home/algo/Documents/print_factory_type/data/991112/991112.jpg +保存截图耗时::0 分 / 6.980211019515991 秒 +执行上 传-parent_dir=/home/algo/Documents/print_factory_type,base_original_obj_dir=/home/algo/Documents/print_factory_type/data/991112,batch_id=991112 +is_use_debug_oss=True +target_dir=/home/algo/Documents/print_factory_type/data/991112, batch_id=991112 +文件已上传到: batchPrint/debug_hsc/991112/991112.json +文件已上传到: batchPrint/debug_hsc/991112/991112.jpg +is_test=True +need_upload_result=False +is_run_local_data=True +pre_batch_id=991112 +print_ids=[85240, 88136] +---------------------------------------- +---------------------------------------- +已删除: /home/algo/Documents/print_factory_type/data/991112/857420_268473Tex1.jpg +已删除: /home/algo/Documents/print_factory_type/data/991112/857420_268473_P85240_5cm_x1.obj +已删除: /home/algo/Documents/print_factory_type/data/991112/875986_305425Tex1.jpg +已删除: /home/algo/Documents/print_factory_type/data/991112/857420_268473.mtl +已删除: /home/algo/Documents/print_factory_type/data/991112/875986_305425_P88136_9cm_x1.obj +已删除: /home/algo/Documents/print_factory_type/data/991112/blend/305425_org.blend +目录 /home/algo/Documents/print_factory_type/data/991112/blend 下的内容已清空。 +已删除子目录: /home/algo/Documents/print_factory_type/data/991112/blend +已删除: /home/algo/Documents/print_factory_type/data/991112/991112.json +已删除: /home/algo/Documents/print_factory_type/data/991112/991112.jpg +已删除: /home/algo/Documents/print_factory_type/data/991112/875986_305425.mtl +目录 /home/algo/Documents/print_factory_type/data/991112 下的内容已清空。 +已删除子目录: /home/algo/Documents/print_factory_type/data/991112 +目录 /home/algo/Documents/print_factory_type/data/ 下的内容已清空。 +目录 /home/algo/Documents/print_factory_type/full/ 下的内容已清空。 +从文件读取了 2 个path +开始下载print_model_info: BatchModelInfo(order_id=875986, pid=305425, print_order_id=88136, model_size='9cm_x1', path='objs/download/print/305425/base/model/9cm', count=1) +target_dir= /home/algo/Documents/print_factory_type/data/991112 +前缀 'objs/download/print/305425/base/model/9cm/' 存在,共找到 6 个对象。 +已更新材质库引用: 305425Tex1.jpg -> 875986_305425Tex1.jpg +下载文件: objs/download/print/305425/base/model/9cm/305425.mtl -> /home/algo/Documents/print_factory_type/data/991112/875986_305425.mtl +下载文件: objs/download/print/305425/base/model/9cm/305425.obj -> /home/algo/Documents/print_factory_type/data/991112/875986_305425_P88136_9cm_x1.obj +下载文件: objs/download/print/305425/base/model/9cm/305425Tex1.jpg -> /home/algo/Documents/print_factory_type/data/991112/875986_305425Tex1.jpg +下载文件: objs/download/print/305425/base/model/9cm/blend/305425_org.blend -> /home/algo/Documents/print_factory_type/data/991112/blend/305425_org.blend +下载 in obj_key +下载 in obj_key +开始下载print_model_info: BatchModelInfo(order_id=857420, pid=268473, print_order_id=85240, model_size='5cm_x1', path='objs/download/print/268473/base_cartoon/badge/1/5cm', count=1) +target_dir= /home/algo/Documents/print_factory_type/data/991112 +前缀 'objs/download/print/268473/base_cartoon/badge/1/5cm/' 存在,共找到 4 个对象。 +已更新材质库引用: 268473Tex1.jpg -> 857420_268473Tex1.jpg +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473.mtl -> /home/algo/Documents/print_factory_type/data/991112/857420_268473.mtl +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473.obj -> /home/algo/Documents/print_factory_type/data/991112/857420_268473_P85240_5cm_x1.obj +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473Tex1.jpg -> /home/algo/Documents/print_factory_type/data/991112/857420_268473Tex1.jpg +下载 in obj_key +下载耗时:4.482503890991211 +selected_machine 小机型 selected_mode 紧凑 output_format JSON +make_bbox_for_print total_time=5.583776950836182 +z_volume_center1=3.494785873362541, z_volume_center2=4.630428442012234 +compute_bbox 857420_268473_P85240_5cm_x1.obj time=2.4529471397399902 +z_volume_center1=7.4748487042688625, z_volume_center2=8.653334486695568 +compute_bbox 875986_305425_P88136_9cm_x1.obj time=2.36457896232605 +all_models [{'name': '857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply', 'dimensions': (50, 50, 9)}, {'name': '875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply', 'dimensions': (90, 25, 19)}] +开始计算排序... +开始计算排序... +⚠️ 单层放置模式:所有模型只能放在平台底面(Z=0) +arrange_models 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply +pre_name1=875986_305425_P88136_9cm, pre_name2= +First Model 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply +model position1 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply (96, 339, 0) +arrange_models 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply +pre_name1=857420_268473_P85240_5cm, pre_name2= +857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply px 150 96 90 +final_y2 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply 56 345 50 6 150 +can_place False 1 cross_border 150, 340, 0, {'name': '857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply', 'dimensions': (50, 50, 9), 'first_line': True}, 380, 345, 250 +model position2 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply (150, 339, 0) +Placed Models: + - 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply at (96, 339, 0) with dimensions (90, 25, 19) + - 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply at (150, 339, 0) with dimensions (50, 50, 9) +Unplaced Models: + - 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply at (96, 339, 0) with dimensions (90, 25, 19) + - 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply at (150, 339, 0) with dimensions (50, 50, 9) +pcd_second : len(pcd_first)=2, len(pcd_second)=0, len(last_pcd_processed)=2, len(pcd_all)=2 +is_test= True +need_upload_result=False +3D打印布局已保存至: /home/algo/Documents/print_factory_type/data/991112/991112.json +排版错误模型数量::0 +排版剩余模型数量::0 +排版完成 +排版总耗时::0 分 / 17.15925693511963 秒 +计算重心::0 分 / 17.15758728981018 秒 +排包围盒::0 分 / 0.0007126331329345703 秒 +挪紧凑::0 分 / 0.00011324882507324219 秒 +移动到位置::0 分 / 0.0008420944213867188 秒 +选择机型=小机型 +已加载并变换: 857420_268473_P85240_5cm_x1.obj +已加载并变换: 875986_305425_P88136_9cm_x1.obj +add_plank /home/algo/Documents/print_factory_type/blank/blank_bias/blank_small.obj +成功加载并变换了 3 个模型 +高级渲染图片已保存到: /home/algo/Documents/print_factory_type/data/991112/991112.jpg +保存截图耗时::0 分 / 7.824073791503906 秒 +执行上 传-parent_dir=/home/algo/Documents/print_factory_type,base_original_obj_dir=/home/algo/Documents/print_factory_type/data/991112,batch_id=991112 +is_use_debug_oss=True +target_dir=/home/algo/Documents/print_factory_type/data/991112, batch_id=991112 +文件已上传到: batchPrint/debug_hsc/991112/991112.json +文件已上传到: batchPrint/debug_hsc/991112/991112.jpg +is_test=True +need_upload_result=False +is_run_local_data=True +pre_batch_id=991112 +print_ids=[85240, 88136] +---------------------------------------- +---------------------------------------- +已删除: /home/algo/Documents/print_factory_type/data/991112/857420_268473Tex1.jpg +已删除: /home/algo/Documents/print_factory_type/data/991112/857420_268473_P85240_5cm_x1.obj +已删除: /home/algo/Documents/print_factory_type/data/991112/875986_305425Tex1.jpg +已删除: /home/algo/Documents/print_factory_type/data/991112/857420_268473.mtl +已删除: /home/algo/Documents/print_factory_type/data/991112/875986_305425_P88136_9cm_x1.obj +已删除: /home/algo/Documents/print_factory_type/data/991112/blend/305425_org.blend +目录 /home/algo/Documents/print_factory_type/data/991112/blend 下的内容已清空。 +已删除子目录: /home/algo/Documents/print_factory_type/data/991112/blend +已删除: /home/algo/Documents/print_factory_type/data/991112/991112.json +已删除: /home/algo/Documents/print_factory_type/data/991112/991112.jpg +已删除: /home/algo/Documents/print_factory_type/data/991112/875986_305425.mtl +目录 /home/algo/Documents/print_factory_type/data/991112 下的内容已清空。 +已删除子目录: /home/algo/Documents/print_factory_type/data/991112 +目录 /home/algo/Documents/print_factory_type/data/ 下的内容已清空。 +目录 /home/algo/Documents/print_factory_type/full/ 下的内容已清空。 +从文件读取了 2 个path +开始下载print_model_info: BatchModelInfo(order_id=875986, pid=305425, print_order_id=88136, model_size='9cm_x1', path='objs/download/print/305425/base/model/9cm', count=1) +target_dir= /home/algo/Documents/print_factory_type/data/991112 +前缀 'objs/download/print/305425/base/model/9cm/' 存在,共找到 6 个对象。 +已更新材质库引用: 305425Tex1.jpg -> 875986_305425Tex1.jpg +下载文件: objs/download/print/305425/base/model/9cm/305425.mtl -> /home/algo/Documents/print_factory_type/data/991112/875986_305425.mtl +下载文件: objs/download/print/305425/base/model/9cm/305425.obj -> /home/algo/Documents/print_factory_type/data/991112/875986_305425_P88136_9cm_x1.obj +下载文件: objs/download/print/305425/base/model/9cm/305425Tex1.jpg -> /home/algo/Documents/print_factory_type/data/991112/875986_305425Tex1.jpg +下载文件: objs/download/print/305425/base/model/9cm/blend/305425_org.blend -> /home/algo/Documents/print_factory_type/data/991112/blend/305425_org.blend +下载 in obj_key +下载 in obj_key +开始下载print_model_info: BatchModelInfo(order_id=857420, pid=268473, print_order_id=85240, model_size='5cm_x1', path='objs/download/print/268473/base_cartoon/badge/1/5cm', count=1) +target_dir= /home/algo/Documents/print_factory_type/data/991112 +前缀 'objs/download/print/268473/base_cartoon/badge/1/5cm/' 存在,共找到 4 个对象。 +已更新材质库引用: 268473Tex1.jpg -> 857420_268473Tex1.jpg +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473.mtl -> /home/algo/Documents/print_factory_type/data/991112/857420_268473.mtl +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473.obj -> /home/algo/Documents/print_factory_type/data/991112/857420_268473_P85240_5cm_x1.obj +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473Tex1.jpg -> /home/algo/Documents/print_factory_type/data/991112/857420_268473Tex1.jpg +下载 in obj_key +下载耗时:4.554089784622192 +selected_machine 小机型 selected_mode 紧凑 output_format JSON +make_bbox_for_print total_time=5.638967514038086 +z_volume_center1=3.494785873362541, z_volume_center2=4.630428442012234 +compute_bbox 857420_268473_P85240_5cm_x1.obj time=2.3993477821350098 +z_volume_center1=7.4748487042688625, z_volume_center2=8.653334486695568 +compute_bbox 875986_305425_P88136_9cm_x1.obj time=2.222135305404663 +all_models [{'name': '857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply', 'dimensions': (50, 50, 9)}, {'name': '875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply', 'dimensions': (90, 25, 19)}] +开始计算排序... +开始计算排序... +⚠️ 单层放置模式:所有模型只能放在平台底面(Z=0) +arrange_models 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply +pre_name1=875986_305425_P88136_9cm, pre_name2= +First Model 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply +model position1 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply (96, 339, 0) +arrange_models 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply +pre_name1=857420_268473_P85240_5cm, pre_name2= +857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply px 150 96 90 +final_y2 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply 56 345 50 6 150 +can_place False 1 cross_border 150, 340, 0, {'name': '857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply', 'dimensions': (50, 50, 9), 'first_line': True}, 380, 345, 250 +model position2 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply (150, 339, 0) +Placed Models: + - 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply at (96, 339, 0) with dimensions (90, 25, 19) + - 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply at (150, 339, 0) with dimensions (50, 50, 9) +Unplaced Models: + - 875986_305425_P88136_9cm_x1=19.759+25.308+90.184.ply at (96, 339, 0) with dimensions (90, 25, 19) + - 857420_268473_P85240_5cm_x1=9.41+49.997+49.997.ply at (150, 339, 0) with dimensions (50, 50, 9) +pcd_second : len(pcd_first)=2, len(pcd_second)=0, len(last_pcd_processed)=2, len(pcd_all)=2 +is_test= True +need_upload_result=False +3D打印布局已保存至: /home/algo/Documents/print_factory_type/data/991112/991112.json +排版错误模型数量::0 +排版剩余模型数量::0 +排版完成 +排版总耗时::0 分 / 16.85209321975708 秒 +计算重心::0 分 / 16.850478410720825 秒 +排包围盒::0 分 / 0.0006732940673828125 秒 +挪紧凑::0 分 / 0.00010204315185546875 秒 +移动到位置::0 分 / 0.0008370876312255859 秒 +选择机型=小机型 +已加载并变换: 857420_268473_P85240_5cm_x1.obj +已加载并变换: 875986_305425_P88136_9cm_x1.obj +add_plank /home/algo/Documents/print_factory_type/blank/blank_bias/blank_small.obj +成功加载并变换了 3 个模型 +高级渲染图片已保存到: /home/algo/Documents/print_factory_type/data/991112/991112.jpg +保存截图耗时::0 分 / 7.028083324432373 秒 +执行上 传-parent_dir=/home/algo/Documents/print_factory_type,base_original_obj_dir=/home/algo/Documents/print_factory_type/data/991112,batch_id=991112 +is_use_debug_oss=True +target_dir=/home/algo/Documents/print_factory_type/data/991112, batch_id=991112 +文件已上传到: batchPrint/debug_hsc/991112/991112.json +文件已上传到: batchPrint/debug_hsc/991112/991112.jpg +is_test=True +need_upload_result=False +is_run_local_data=True +pre_batch_id=991112 +print_ids=[85240, 88136] +---------------------------------------- +---------------------------------------- +已删除: /home/algo/Documents/print_factory_type/data/991112/857420_268473Tex1.jpg +已删除: /home/algo/Documents/print_factory_type/data/991112/857420_268473_P85240_5cm_x1.obj +已删除: /home/algo/Documents/print_factory_type/data/991112/875986_305425Tex1.jpg +已删除: /home/algo/Documents/print_factory_type/data/991112/857420_268473.mtl +已删除: /home/algo/Documents/print_factory_type/data/991112/875986_305425_P88136_9cm_x1.obj +已删除: /home/algo/Documents/print_factory_type/data/991112/blend/305425_org.blend +目录 /home/algo/Documents/print_factory_type/data/991112/blend 下的内容已清空。 +已删除子目录: /home/algo/Documents/print_factory_type/data/991112/blend +已删除: /home/algo/Documents/print_factory_type/data/991112/991112.json +已删除: /home/algo/Documents/print_factory_type/data/991112/991112.jpg +已删除: /home/algo/Documents/print_factory_type/data/991112/875986_305425.mtl +目录 /home/algo/Documents/print_factory_type/data/991112 下的内容已清空。 +已删除子目录: /home/algo/Documents/print_factory_type/data/991112 +目录 /home/algo/Documents/print_factory_type/data/ 下的内容已清空。 +目录 /home/algo/Documents/print_factory_type/full/ 下的内容已清空。 +从文件读取了 2 个path +开始下载print_model_info: BatchModelInfo(order_id=857420, pid=268473, print_order_id=85240, model_size='5cm_x1', path='objs/download/print/268473/base_cartoon/badge/1/5cm', count=1) +target_dir= /home/algo/Documents/print_factory_type/data/991112 +前缀 'objs/download/print/268473/base_cartoon/badge/1/5cm/' 存在,共找到 4 个对象。 +已更新材质库引用: 268473Tex1.jpg -> 857420_268473Tex1.jpg +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473.mtl -> /home/algo/Documents/print_factory_type/data/991112/857420_268473.mtl +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473.obj -> /home/algo/Documents/print_factory_type/data/991112/857420_268473_P85240_5cm_x1.obj +下载文件: objs/download/print/268473/base_cartoon/badge/1/5cm/268473Tex1.jpg -> /home/algo/Documents/print_factory_type/data/991112/857420_268473Tex1.jpg +下载 in obj_key +开始下载print_model_info: BatchModelInfo(order_id=875986, pid=305425, print_order_id=88136, model_size='9cm_x1', path='objs/download/print/305425/base/model/9cm', count=1) +target_dir= /home/algo/Documents/print_factory_type/data/991112 +前缀 'objs/download/print/305425/base/model/9cm/' 存在,共找到 6 个对象。 diff --git a/point_cloud_layout.py b/point_cloud_layout.py new file mode 100644 index 0000000..9e77fba --- /dev/null +++ b/point_cloud_layout.py @@ -0,0 +1,1567 @@ +import os +import json +import requests +import shutil +import time +import random +import matplotlib.pyplot as plt +import open3d as o3d +import numpy as np +from plyfile import PlyData, PlyElement + +from config import is_test +from config import print_factory_type_dir + +from general import mesh_tranform_to_pcd +from general import need_upload_result + +from compute_print_net import get_models_bbox +from compute_print_net import arrange_models_on_platform +from compute_print_net import Platform +from compute_print_net import down_sample +from compute_print_net import read_mesh +from compute_print_net import compute_bbox_all +from compute_print_net import compute_bbox_all_ext + +def make_bbox_for_print(base_original_obj_dir,dict_bad,dict_origin,compact_min_dis): + """获取需要的盒子大小""" + start_time = time.time() + + obj_id_list = [aa.split(".o")[0] for aa in os.listdir(base_original_obj_dir) if aa.endswith(".obj")] + obj_id_list = obj_id_list + + dict_mesh_obj = {} + for pid_t_y in obj_id_list: + + obj_name = pid_t_y+".obj" + obj_path = os.path.join(base_original_obj_dir,obj_name) + mesh_obj = read_mesh(obj_path) + + if mesh_obj is None: + dict_bad[obj_name]=obj_name + # 记录错误文件 + error_log = os.path.join(base_original_obj_dir, "error_files.txt") + with open(error_log, 'a') as f: + f.write(f"{obj_path}\n") + print(f"Skipping invalid file: {obj_path}") + continue + + # dict_origin[obj_path] = copy.deepcopy(mesh_obj) + dict_origin[obj_name] = copy.deepcopy(mesh_obj) + dict_mesh_obj[obj_name] = mesh_obj + + print(f"make_bbox_for_print total_time={time.time()-start_time}") + + dict_total_matrix,all_models = compute_bbox_all_ext(base_original_obj_dir, compact_min_dis) + # dict_total_matrix,all_models = compute_bbox_all(dict_mesh_obj, compact_min_dis) + return get_dict_pcd(dict_mesh_obj,dict_total_matrix,all_models) + +def get_dict_pcd(dict_mesh_obj,dict_total_matrix,all_models): + dict_pcd_fix = {} + + dict_ply_name = {} + for model in all_models: + ply_name = model['name'] + dict_ply_name[f"{ply_name.split("=")[0]}.obj"] = ply_name + + dict_pcd_fix = get_pcd_by_matrix(dict_mesh_obj,dict_total_matrix,dict_ply_name) + + return dict_total_matrix,all_models,dict_pcd_fix + +def get_pcd_by_matrix(dict_mesh_obj,dict_total_matrix,dict_ply_name): + + dict_pcd_fix= {} + + for key, value in dict_mesh_obj.items(): + obj_name = key + mesh_obj = value + pcd_fix = mesh_tranform_to_pcd(mesh_obj, dict_total_matrix[obj_name]) + dict_pcd_fix[dict_ply_name[obj_name]] = pcd_fix + + return dict_pcd_fix + + +def ply_print_layout_platform(dict_pcd_fix,dict_pcd_fix2,machine_size,dict_total_matrix,all_models): + """根据排版结果移动点云到指定位置""" + # placed_models,unplaced_models = get_models_box_size(dict_fix,machine_size) + + # 1. 获取模型bbox尺寸 + # all_models = get_models_bbox_net(dict_pcd_fix) + print("all_models", all_models) + + # 2. 模型排版布局 + print("开始计算排序...") + placed_models, unplaced_models = arrange_models_on_platform(all_models, machine_size) + + if len(placed_models) ==0: + print("放进打印盒的数量为0") + return + + # 3. 根据排版结果移动点云和模型(原有逻辑不变) + for model in placed_models: + print(f" - {model['name']} at {model['position']} with dimensions {model['dimensions']}") + ply_file_name = model['name'] + move_position = model['position'] + # print("要读取的点云数据路径",ply_origin_path) + pcd = dict_pcd_fix[ply_file_name] + # print("dict_fix read",ply_file_name,move_position) + + points = np.asarray(pcd.points) + min_bound = np.min(points, axis=0) # 获取点云的最小边界 + max_bound = np.max(points, axis=0) + min_bound[1] = max(min_bound[1], 0) + bbox_center = (min_bound + max_bound) / 2 # 计算包围盒的中心点 + bbox_extent = (max_bound - min_bound) + new_bbox = o3d.geometry.OrientedBoundingBox(center=bbox_center, + R=np.eye(3), # 旋转矩阵,默认没有旋转 + extent=bbox_extent) + x = move_position[0] + y = move_position[1] + z = move_position[2] + # move_position = np.array([x,y,z])/100 + move_position = np.array([x, y, z]) + # translation_vector = -move_position + translation_vector = move_position + pcd.translate(translation_vector) + new_bbox.translate(translation_vector) + obj_name = ply_file_name.split("=")[0]+".obj" + + T_trans1 = np.eye(4) + T_trans1[:3, 3] = translation_vector + dict_total_matrix[obj_name]= T_trans1 @ dict_total_matrix[obj_name] + + new_bbox_lines = o3d.geometry.LineSet.create_from_oriented_bounding_box(new_bbox) + new_bbox_lines.paint_uniform_color([1, 0, 0]) # 红色 + + dict_pcd_fix2[ply_file_name] = pcd + + return placed_models + +def get_models_bbox_net(dict_pcd_fix): + return get_models_bbox(dict_pcd_fix) + +""" +def get_models_box_size(dict_fix,machine_size): + #获取排版的盒子大小 + models = [] + for ply_file in dict_fix: + bbox_with_text = ply_file.split("=") + bbox_with = bbox_with_text[-1] + split_text = bbox_with.replace(".ply","").split("+") + extend_dist = 2 + x_length = int(float(split_text[2])*100) + extend_dist + y_length = int(float(split_text[0])*100) + extend_dist + z_length = int(float(split_text[1])*100) + extend_dist + #print("get_models_box_size",x_length,y_length,z_length) + models.append({'name':ply_file,'dimensions':(int(x_length/100),int(z_length/100),int(y_length/100))}) + print(models) + + platform = Platform(int(machine_size[0]), int(machine_size[1]), int(machine_size[2])) + print("开始计算排序...") + platform.arrange_models(models) + platform.print_results() + return platform.get_result() +""" + +def compute_distance(pcd1, pcd2): + """ + 正确计算两个点云之间距离的函数。 + 返回两个点云之间最近距离的平均值、最小值以及全部距离数组。 + """ + # 使用Open3D内置的高效方法计算距离 + # 计算pcd1中每个点到pcd2中最近点的距离 + distances = pcd1.compute_point_cloud_distance(pcd2) + distances = np.asarray(distances) + + # 计算有意义的统计量 + min_dist = np.min(distances) # 所有点中的最小距离 + mean_dist = np.mean(distances) # 距离的平均值 + + # return min_dist, mean_dist, distances + return min_dist + +def compute_distance_x(pcd1, pcd2): + points1 = np.asarray(pcd1.points)[:, 0] # 提取所有X坐标[3](@ref) + points2 = np.asarray(pcd2.points)[:, 0] + + x_diff = np.abs(points1[:, np.newaxis] - points2) + return np.min(x_diff) + +def compute_distance_y(pcd1, pcd2): + points1 = np.asarray(pcd1.points)[:, 1] # 提取所有Y坐标[3](@ref) + points2 = np.asarray(pcd2.points)[:, 1] + + y_diff = np.abs(points1.reshape(-1, 1) - points2) + return np.min(y_diff) + +def check_collision_x(pcd_moving, static_pcds,collision_threshold): + moving_points = np.asarray(pcd_moving.points) + min_distance_to_x_axis = np.min(np.abs(moving_points[:, 1])) # Y 坐标即为与 X 轴的距离 + #print(f"与 X 轴的最小距离: {min_distance_to_x_axis}") + #print(f"与 Y 轴的最小距离: {min_distance_to_y_axis}") + #print(f"pcd_moving{len(static_pcds)}") + if min_distance_to_x_axis < collision_threshold: + print(f"与 X 轴发生碰撞! 最小距离: {min_distance_to_x_axis}") + return True + + return check_collision_all(pcd_moving, static_pcds,collision_threshold) + +def check_collision_y(pcd_moving, static_pcds,collision_threshold): + moving_points = np.asarray(pcd_moving.points) + #print(f"与 X 轴的最小距离: {min_distance_to_x_axis}") + min_distance_to_y_axis = np.min(np.abs(moving_points[:, 0])) # X 坐标即为与 Y 轴的距离 + #print(f"与 Y 轴的最小距离: {min_distance_to_y_axis}") + #print(f"pcd_moving{len(static_pcds)}") + if min_distance_to_y_axis < collision_threshold: + print(f"与 Y 轴发生碰撞! 最小距离: {min_distance_to_y_axis}") + return True + + return check_collision_all(pcd_moving, static_pcds,collision_threshold) + +import numpy as np + +def compute_aabb(pcd): + """计算点云的AABB包围盒""" + points = np.asarray(pcd.points) + return { + 'min': np.min(points, axis=0), + 'max': np.max(points, axis=0) + } + +def aabb_intersect(a, b, collision_threshold): + """判断两个AABB包围盒是否相交[2,8](@ref)""" + return (a['max'][0] > b['min'][0] - collision_threshold and a['min'][0] < b['max'][0] + collision_threshold) and \ + (a['max'][1] > b['min'][1] - collision_threshold and a['min'][1] < b['max'][1] + collision_threshold) and \ + (a['max'][2] > b['min'][2] - collision_threshold and a['min'][2] < b['max'][2] + collision_threshold) + +def check_collision_all(pcd_moving, static_pcds, collision_threshold): + # 预计算移动点云AABB + moving_aabb = compute_aabb(pcd_moving) + + for static_pcd in static_pcds: + if static_pcd == pcd_moving: + continue + # 第一阶段:AABB快速排除[1,6](@ref) + static_aabb = compute_aabb(static_pcd) + # print("len(static_pcd.points)=",len(static_pcd.points),"len(moving_aabb.points)=",len(pcd_moving.points)) + if not aabb_intersect(moving_aabb, static_aabb, collision_threshold): + continue # 包围盒无交集,直接跳过 + + if not aabb_intersect(moving_aabb, static_aabb, collision_threshold): + return False + + # 第二阶段:精确点距离计算 + min_distance = compute_distance(pcd_moving, static_pcd) + # print("check_collision_all",min_distance) + if min_distance < collision_threshold: + return True + + return False + +def compute_centroid(pcd): + # 获取点云的所有点 + points = np.asarray(pcd.points) + # 计算质心(只考虑 X 和 Y 坐标) + centroid = np.mean(points[:, :2], axis=0) # 只考虑前两个维度(X 和 Y) + return centroid + +def compute_distance_to_origin(centroid): + # 计算质心距离原点的距离(只考虑 X 和 Y 坐标) + return np.linalg.norm(centroid) # 计算 X 和 Y 的欧几里得距离 + +def compute_closest_distance_to_origin(pcd): + # 获取点云的所有点坐标 + points = np.asarray(pcd.points) + # 计算每个点到原点的距离 + distances = np.linalg.norm(points, axis=1) + # 返回最小距离 + return np.min(distances) + +def sort_ply_files_by_closest_distance(folder_path): + ply_files = [f for f in os.listdir(folder_path) if f.endswith('.ply')] + distances = [] + + for ply_file in ply_files: + # 读取点云数据 + pcd = o3d.io.read_point_cloud(os.path.join(folder_path, ply_file)) + # 计算离原点最近的点的距离 + closest_distance = compute_closest_distance_to_origin(pcd) + distances.append((ply_file, closest_distance)) + + # 按照最近点的距离排序(由近到远) + distances.sort(key=lambda x: x[1]) + + # 返回排序后的文件列表 + sorted_files = [item[0] for item in distances] + print("Sorted files:", sorted_files) + return sorted_files + +def compact_mode_for_min_dis_json(placed_models,dict_unplaced,dict_bounds_fix,machine_size,dict_total_matrix): + + y_step=1 + x_step=1 + delta = 10 + + edge_x_min=0 + 4 + edge_y_min=0 + 4 + edge_x_max=machine_size[0] + edge_y_max=machine_size[1] + + collision_threshold=2 + move_last = True + + pcd_all = [] + pcd_processed = [] + pcd_processed_x_top = [] + pcd_processed_no_x_top = [] + # name_list = [] + dict_name = {} + # model_list = [] + dict_model = {} + last_pcd_list = [] + # last_name_list = [] + dic_last_name = {} + last_pcd_processed = [] + max_x = machine_size[0] + min_x = 0 + max_delta_x = 0 + x_top_delta = 1 + border_delta = 4 + + pcd_first= [] + pcd_second= [] + + index = 0 + for model in placed_models: + pcd = dict_bounds_fix[model['name']] + + pcd_all.append(pcd) + + if (get_axis_aligned_bbox(pcd)['y_min']>edge_y_max*0.3 or True): + pcd_first.append(pcd) + else: + pcd_second.append(pcd) + + # pcd_all.append(pcd_downsampled) + # name_list.append(model['name']) + # model_list.append(model) + dict_name[pcd] = model['name'] + dict_model[pcd] = model + dx = model['dimensions'][0] + x = model['position'][0] + + if (x>=edge_x_max-x_top_delta) : + pcd_processed_x_top.append(pcd) + print("pcd_processed_x_top", model['name']) + if dx > max_x: + max_x = dx + if dx < min_x: + min_x = dx + max_delta_x = max_x - min_x + + index += 1 + + # print("compact_mode_for_min_dis1_json", model, max_delta_x) + + draw_down = False + if max_delta_x < 10: + draw_down = False + + # for idx, pcd in enumerate(pcd_all): + for idx, pcd in enumerate(pcd_first): + + if dict_model[pcd]['first_line']: + pcd_processed.append(pcd) + last_pcd_processed.append(pcd) + continue + + x = dict_model[pcd]['position'][0] + y = dict_model[pcd]['position'][1] + dx = dict_model[pcd]['dimensions'][0] + print("compact_mode", dict_name[pcd], dx, x) + + ply_file_name = dict_name[pcd] + obj_name = ply_file_name.split("=")[0]+".obj" + + T_trans1 = np.eye(4) + + dist_x = 50 + dist_y = 20 + is_x_top = False + if x - 10 < edge_x_min: + dist_x = x - edge_x_min + if y - 10 < edge_y_min: + dist_y = y - edge_y_min + if (x 80: + y_init_big = 10 + x_init_big = y_init_big - 1 + + if check_collision_all(pcd, pcd_processed_curr, 1): + while True: + step = 25 + pcd.translate([0, -step, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -step, 0] + T_trans1 = T_transTemp @ T_trans1 + # pcd.translate([-step, 0, 0]) + # T_transTemp[:3, 3] = [-step, 0, 0] + # T_trans1 = T_transTemp @ T_trans1 + + if not check_collision_all(pcd, pcd_processed_curr, collision_threshold_big): + break + + """ + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['y_max'] >= edge_y_max - collision_threshold: + pcd.translate([-x_step_big, -y_step_big, 0]) + print("compact_mode y_max", idx, bbox['y_max'], edge_y_max - collision_threshold_big) + break + if bbox['x_max'] >= edge_x_max - collision_threshold: + pcd.translate([-x_step_big, -y_step_big, 0]) + print("compact_mode x_max", idx, bbox['x_max'], edge_x_max - collision_threshold_big) + break + if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big): + pcd.translate([-x_step_big, -y_step_big, 0]) + break + pcd.translate([x_step_big, y_step_big, 0]) + #""" + #""" + while True: + bbox = get_axis_aligned_bbox(pcd) + # print("x_max",bbox['x_max'],bbox['x_min'],bbox['y_max'],bbox['y_min']) + if bbox['y_min'] <= edge_y_min + collision_threshold_big and False: + pcd.translate([0, y_step_big, 0]) + break + if bbox['y_max'] >= edge_y_max - collision_threshold_big: + pcd.translate([0, -y_step_big, 0]) + + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + + break + if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big+y_init_big): #5 + pcd.translate([0, -y_step_big, 0]) + + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + + break + pcd.translate([0, y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['x_min'] <= edge_x_min + collision_threshold_big and False: + pcd.translate([x_step_big, 0, 0]) + break + if bbox['x_max'] >= edge_x_max - collision_threshold_big: + pcd.translate([-x_step_big, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [-x_step_big, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big+x_init_big): + pcd.translate([-x_step_big, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [-x_step_big, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + break + pcd.translate([x_step_big, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [x_step_big, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + #""" + #""" + collision_threshold_init = collision_threshold+6 + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: + pcd.translate([0, y_step, 0]) + break + # if bbox['y_max'] >= edge_y_max - collision_threshold_init: + if bbox['y_max'] >= edge_y_max - border_delta: + pcd.translate([0, -y_step, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init+1): #5 + pcd.translate([0, -y_step, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step, 0] + T_trans1 = T_transTemp @ T_trans1 + break + pcd.translate([0, y_step, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, y_step, 0] + T_trans1 = T_transTemp @ T_trans1 + #""" + #""" + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['x_min'] <= edge_x_min + collision_threshold_init and False: + pcd.translate([x_step, 0, 0]) + break + # if bbox['x_max'] >= edge_x_max - collision_threshold_init: + if bbox['x_max'] >= edge_x_max - border_delta: + # print("1pcd.translate([-x_step, 0, 0])",name_list[idx]) + pcd.translate([-x_step, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [-x_step, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): + # print("2pcd.translate([-x_step, 0, 0])",name_list[idx]) + pcd.translate([-x_step, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [-x_step, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + break + pcd.translate([x_step, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [x_step, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + #""" + #""" + collision_threshold_init = collision_threshold+2 + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: + pcd.translate([0, y_step, 0]) + break + if bbox['y_max'] >= edge_y_max - collision_threshold_init: + pcd.translate([0, -y_step, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init+1): #5 + pcd.translate([0, -y_step, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step, 0] + T_trans1 = T_transTemp @ T_trans1 + break + pcd.translate([0, y_step, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, y_step, 0] + T_trans1 = T_transTemp @ T_trans1 + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['x_min'] <= edge_x_min + collision_threshold_init and False: + pcd.translate([x_step, 0, 0]) + break + if bbox['x_max'] >= edge_x_max - collision_threshold_init: + pcd.translate([-x_step, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [-x_step, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): + pcd.translate([-x_step, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [-x_step, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + break + pcd.translate([x_step, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [x_step, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + # place again + collision_threshold_init = collision_threshold+1 + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: + pcd.translate([0, y_step, 0]) + break + if bbox['y_max'] >= edge_y_max - collision_threshold_init: + pcd.translate([0, -y_step, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): #5 + pcd.translate([0, -y_step, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step, 0] + T_trans1 = T_transTemp @ T_trans1 + break + pcd.translate([0, y_step, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, y_step, 0] + T_trans1 = T_transTemp @ T_trans1 + #""" + pcd_processed.append(pcd) + pcd_processed_x_top.append(pcd) + if not is_x_top: + pcd_processed_no_x_top.append(pcd) + + cross_border = False + bbox = get_axis_aligned_bbox(pcd) + if bbox['x_min'] <= edge_x_min + 1 or bbox['y_min'] <= edge_y_min + 1: + cross_border = True + print("coross_border",ply_file_name) + + if cross_border: + pcd_second.append(pcd) + dic_last_name[pcd] = ply_file_name + else: + last_pcd_processed.append(pcd) + + dict_total_matrix[obj_name]= T_trans1 @ dict_total_matrix[obj_name] + + volumes = [] + # for idx, pcd in enumerate(last_pcd_list): + for idx, pcd in enumerate(pcd_second): + bbox = get_axis_aligned_bbox(pcd) + + x_length = bbox['x_max'] - bbox['x_min'] + y_length = bbox['y_max'] - bbox['y_min'] + z_length = bbox['z_max'] - bbox['z_min'] + volume = x_length * y_length * z_length + volumes.append(volume) + + # print("last_pcd_list", len(last_pcd_list), len(last_pcd_list), len(last_pcd_processed), len(pcd_all)) + # sorted_indices = np.argsort(volumes)[::-1] + # last_pcd_list2 = [last_pcd_list[i] for i in sorted_indices] + # print("last_pcd_list2", len(last_pcd_list2)) + + print(f"pcd_second : len(pcd_first)={len(pcd_first)}, len(pcd_second)={len(pcd_second)}, len(last_pcd_processed)={len(last_pcd_processed)}, len(pcd_all)={len(pcd_all)}") + sorted_indices = np.argsort(volumes)[::-1] + pcd_second2 = [pcd_second[i] for i in sorted_indices] + # print("pcd_second2 len", len(pcd_second2)) + + for idx, pcd in enumerate(pcd_second2): + + ply_file_name = dict_name[pcd] + obj_name = ply_file_name.split("=")[0]+".obj" + + # print("pcd_second2", obj_name) + + T_trans1 = np.eye(4) + + points = np.asarray(pcd.points) + min_x = np.min(points[:, 0]) + max_y = np.max(points[:, 1]) # 当前最大y值 + + tx = edge_x_min - min_x + ty = -max_y - 0.001 + + T_transTemp = move_to_top_left(pcd, edge_x_min+2, edge_y_max-2) + + T_trans1 = T_transTemp @ T_trans1 + + name = dict_name[pcd] + # print("pcd_second2",name,"tx",tx,"ty",ty) + succ_move = True + y_accum = 0 + finish_move2 = False + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+1 + if bbox['y_min'] <= edge_y_min + collision_threshold_init: + print("succ_move False",name,bbox['y_min'],edge_y_min + collision_threshold_init) + succ_move = False + finish_move2 = True + + if (finish_move2): + break + else: + if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): + print("succ_move1",name,bbox['x_max'],bbox['y_max'],len(last_pcd_processed)) + break + + pcd.translate([0, -y_step, 0]) + + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step, 0] + T_trans1 = T_transTemp @ T_trans1 + + y_accum += y_step + + if succ_move: + #print("succ_move2", name) + """ + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['x_max'] >= edge_x_max - collision_threshold_big: + pcd.translate([-x_step_big, 0, 0]) + break + if check_collision_all(pcd, last_pcd_processed,collision_threshold_big): + pcd.translate([-x_step_big, 0, 0]) + break + pcd.translate([x_step_big, 0, 0]) + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['y_max'] >= edge_y_max - collision_threshold: + pcd.translate([0, -y_step, 0]) + break + if check_collision_all(pcd, last_pcd_processed,collision_threshold+1): #5 + pcd.translate([0, -y_step, 0]) + break + pcd.translate([0, y_step, 0]) + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['x_max'] >= edge_x_max - collision_threshold: + pcd.translate([-x_step, 0, 0]) + break + if check_collision_all(pcd, last_pcd_processed,collision_threshold): + pcd.translate([-x_step, 0, 0]) + break + pcd.translate([x_step, 0, 0]) + #""" + + #""" + x_accu = 0 + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+2 + #print("Move x_step_big", name, bbox['y_max'], bbox['x_max']) + if bbox['x_max'] >= edge_x_max - collision_threshold_init: + pcd.translate([-x_accu, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [-x_accu, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): + x_accu = 0 + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+1 + if bbox['y_max'] >= edge_y_max - collision_threshold_init: + pcd.translate([0, -y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + #print("Move y_max", name, bbox['y_max'], bbox['x_max']) + break + if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): + pcd.translate([0, -y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + #print("Move y_max2", name, bbox['y_max'], bbox['x_max']) + break + pcd.translate([0, y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + #print("Move y_step_big", name, bbox['y_max'], bbox['x_max']) + else: + n = 1 + + x_accu += x_step_big + pcd.translate([x_step_big, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [x_step_big, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + #""" + else: + # T_transTemp = move_to_bottom_left(pcd, edge_x_min, edge_y_min) + T_transTemp = move_to_bottom_right(pcd, edge_x_max, edge_y_min) + T_trans1 = T_transTemp @ T_trans1 + + print("last place", name) + + """ + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+10 + if bbox['x_max'] >= edge_x_max - collision_threshold_init: + print("fail to place",name) + break + if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): + print("last place2",name) + break + pcd.translate([x_step_big, 0, 0]) + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+3 + if bbox['y_max'] >= edge_y_max - collision_threshold_init: + pcd.translate([0, -y_step_big, 0]) + print("last place3",name) + break + if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): #5 + pcd.translate([0, -y_step_big, 0]) + print("last place4",name,collision_threshold_init,len(last_pcd_processed)) + break + pcd.translate([0, y_step_big, 0]) + print("last place41",y_step_big) + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['y_max'] >= edge_y_max - collision_threshold: + pcd.translate([0, -y_step, 0]) + print("last place5",name) + break + if check_collision_all(pcd, last_pcd_processed,collision_threshold+1): #5 + pcd.translate([0, -y_step, 0]) + print("last place6",name) + break + pcd.translate([0, y_step, 0]) + while True: + bbox = get_axis_aligned_bbox(pcd) + if bbox['x_max'] >= edge_x_max - collision_threshold: + pcd.translate([-x_step, 0, 0]) + print("last place7",name) + break + if check_collision_all(pcd, last_pcd_processed,collision_threshold): + pcd.translate([-x_step, 0, 0]) + print("last place8",name) + break + pcd.translate([x_step, 0, 0]) + #""" + + """ + can_place_last = False + x_accu = 0 + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+2 + if bbox['x_max'] >= edge_x_max - collision_threshold_init: + if not can_place_last: + print("fail to place",name) + dict_unplaced[name]=name + else: + pcd.translate([-x_accu, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [-x_accu, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): + can_place_last = True + x_accu = 0 + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+1 + if bbox['y_max'] >= edge_y_max - collision_threshold_init: + pcd.translate([0, -y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): + pcd.translate([0, -y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + break + pcd.translate([0, y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + #print("Move2 y_step_big", name) + else: + n = 1 + + x_accu += x_step_big + pcd.translate([x_step_big, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [x_step_big, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + #""" + + #""" + can_place_last = False + x_accu = 0 + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+2 + if bbox['x_max'] >= edge_x_max - collision_threshold_init: + if not can_place_last: + print("fail to place",name) + dict_unplaced[name]=name + else: + pcd.translate([-x_accu, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [-x_accu, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): + can_place_last = True + x_accu = 0 + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+1 + if bbox['y_max'] >= edge_y_max - collision_threshold_init: + pcd.translate([0, -y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + break + if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): + pcd.translate([0, -y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + break + pcd.translate([0, y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + #print("Move2 y_step_big", name) + else: + n = 1 + + x_accu += x_step_big + pcd.translate([x_step_big, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [x_step_big, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + #""" + + """ + can_place_last = False + x_accu = 0 + place_first = True + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+2 + if bbox['x_min'] <= edge_x_min + collision_threshold_init: + if not can_place_last: + print("fail to place",name) + dict_unplaced[name]=name + else: + pcd.translate([+x_accu, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [+x_accu, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + place_first = True + print("place_first True") + break + can_break = False + if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): + can_place_last = True + x_accu = 0 + while True: + bbox = get_axis_aligned_bbox(pcd) + collision_threshold_init = collision_threshold+1 + if bbox['y_max'] >= edge_y_max - collision_threshold_init: + pcd.translate([0, -y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + can_break = True + break + if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): + pcd.translate([0, -y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, -y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + if place_first: + can_break = True + break + pcd.translate([0, y_step_big, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [0, y_step_big, 0] + T_trans1 = T_transTemp @ T_trans1 + #print("Move2 y_step_big", name) + else: + n = 1 + + if can_break: + break + + x_accu += x_step_big + pcd.translate([-x_step_big, 0, 0]) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [-x_step_big, 0, 0] + T_trans1 = T_transTemp @ T_trans1 + #""" + + last_pcd_processed.append(pcd) + """ + print("is_x_top",is_x_top) + if not is_x_top: + last_pcd_processed.append(pcd) + print("last_pcd_processed.append",name) + else: + print("fail last_pcd_processed.append",name, is_x_top) + """ + # o3d.io.write_point_cloud(os.path.join(output_dir, name), pcd) + + dict_total_matrix[obj_name]= T_trans1 @ dict_total_matrix[obj_name] + +def move_to_top_left(pcd, edge_x_min, edge_y_max): + points = np.asarray(pcd.points) + min_x = np.min(points[:, 0]) + max_y = np.max(points[:, 1]) # 当前最大y值 + + tx = edge_x_min - min_x + # ty = -max_y - 0.001 + ty = edge_y_max - max_y + + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [tx, ty, 0] + + pcd.translate((tx, ty, 0), relative=True) + + return T_transTemp + +def move_to_bottom_left(pcd, edge_x_min, edge_y_min): + + points = np.asarray(pcd.points) + min_x = np.min(points[:, 0]) + min_y = np.min(points[:, 1]) + tx = edge_x_min - min_x + ty = edge_y_min - min_y + pcd.translate((tx, ty, 0), relative=True) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [tx, ty, 0] + + return T_transTemp + +def move_to_bottom_right(pcd, edge_x_max, edge_y_min): + + points = np.asarray(pcd.points) + max_x = np.min(points[:, 0]) + min_y = np.min(points[:, 1]) + tx = edge_x_max - max_x + ty = edge_y_min - min_y + pcd.translate((tx, ty, 0), relative=True) + T_transTemp = np.eye(4) + T_transTemp[:3, 3] = [tx, ty, 0] + + return T_transTemp + +def get_axis_aligned_bbox(pcd): + points = np.asarray(pcd.points) + return { + 'x_min': np.min(points[:,0]), + 'x_max': np.max(points[:,0]), + 'y_min': np.min(points[:,1]), + 'y_max': np.max(points[:,1]), + 'z_min': np.min(points[:,2]), + 'z_max': np.max(points[:,2]) + } + +def down_obj_data_to_ply(weight_fix_out_obj_dir,weight_fix_out_ply_dir): + """""" + obj_file_list = [aa for aa in os.listdir(weight_fix_out_obj_dir) if aa.endswith(".obj")] + for obj_name in obj_file_list: + obj_path = os.path.join(weight_fix_out_obj_dir,obj_name) + + mesh_obj = read_mesh(obj_path) + + vertices = np.asarray(mesh_obj.vertices) + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(vertices) + voxel_size = 3 # 设置体素的大小,决定下采样的密度 + pcd_downsampled = down_sample(pcd, voxel_size) + ply_out_path = os.path.join(weight_fix_out_ply_dir,obj_name.replace(".obj",".ply")) + o3d.io.write_point_cloud(ply_out_path, pcd_downsampled) + print(ply_out_path,"下采样完成。") + +def compute_centroid_compact(pcd): + points = np.asarray(pcd.points) + centroid = np.mean(points, axis=0) + return centroid + +def compute_base_point(pcd): + points = np.asarray(pcd.points) + x_center = np.mean(points[:, 0]) + y_center = np.mean(points[:, 1]) + min_z = np.min(points[:, 2]) + return np.array([x_center, y_center, min_z]) + +import copy +def move_obj_to_compact_bounds(bounds_fix_out_dir,bounds_compact_out_dir,weight_fix_out_obj_dir,base_original_obj_dir,compact_obj_out_dir,dict_mesh_obj,dict_unplaced,placed_remove_dir,dict_bad,bad_dir,full_dir,dict_bounds_fix,dict_compact,dict_origin): + """""" + # obj_file_list = [aa for aa in os.listdir(weight_fix_out_obj_dir) if aa.endswith(".obj")] + obj_file_list = list(dict_mesh_obj.keys()) + ply_path_dict = {} + + # meshes = [] + # for ply_file_name in os.listdir(bounds_fix_out_dir): + for ply_file_name in dict_bounds_fix: + ply_dict_key = ply_file_name.split("=")[0] + ply_path_dict[ply_dict_key] = ply_file_name + for obj_name in obj_file_list: + obj_path = os.path.join(weight_fix_out_obj_dir, obj_name) + + # mesh_obj = read_mesh(obj_path, False) + mesh_obj = dict_mesh_obj[obj_name] + # mesh_obj = dict_origin[obj_origin_path] + + original_obj_pid_dir = base_original_obj_dir + obj_origin_path = os.path.join(original_obj_pid_dir, obj_name) + obj_origin = dict_origin[obj_origin_path] + # obj_origin = copy.deepcopy(dict_origin[obj_origin_path]) + + ply_name_pid = obj_name.replace(".obj","") + # ply_name = ply_path_dict[ply_name_pid] + ply_name = ply_path_dict.get(ply_name_pid,None) + print(ply_name_pid,ply_name) + if ply_name is None: + continue + print("move_obj_to_compact_bounds",ply_name,len(dict_unplaced)) + if not ply_name or ply_name in dict_unplaced: + print("unplaced",ply_name) + continue + ply_fix_path = os.path.join(bounds_fix_out_dir,ply_name) + ply_compact_path = os.path.join(bounds_compact_out_dir, ply_name) + # pcd_fix = o3d.io.read_point_cloud(ply_fix_path) + pcd_fix = dict_bounds_fix[ply_name] + + vertices = np.asarray(obj_origin.vertices) + pcd_origin = o3d.geometry.PointCloud() + pcd_origin.points = o3d.utility.Vector3dVector(vertices) + + # pcd_compact = o3d.io.read_point_cloud(ply_compact_path) + pcd_compact = dict_compact[ply_name] + + centroid_fix = compute_centroid_compact(pcd_fix) + centroid_compact = compute_centroid_compact(pcd_compact) + centroid_origin = compute_centroid_compact(pcd_origin) + displacement = centroid_compact - centroid_fix + # displacement = centroid_compact - centroid_origin + vertices = np.asarray(mesh_obj.vertices) + vertices_translated = vertices + displacement # 将位移应用到每个顶点 + mesh_obj.vertices = o3d.utility.Vector3dVector(vertices_translated) # 更新网格顶点 + + obj_pid = obj_name.split("_P")[0] + #compact_obj_pid_out_dir = os.path.join(compact_obj_out_dir,obj_pid) + compact_obj_pid_out_dir= compact_obj_out_dir + if not os.path.exists(compact_obj_pid_out_dir): + os.makedirs(compact_obj_pid_out_dir) + obj_path_compact = os.path.join(compact_obj_pid_out_dir,obj_name) + mesh_obj.compute_vertex_normals() + o3d.io.write_triangle_mesh(obj_path_compact, mesh_obj,write_triangle_uvs=True) + print(obj_path_compact, "移动后obj保存完成", displacement) + + # meshes.append(mesh_obj) + + #original_obj_pid_dir = os.path.join(base_original_obj_dir,obj_pid) + original_obj_pid_dir = base_original_obj_dir + for mtl in os.listdir(compact_obj_pid_out_dir): + if mtl.endswith(".mtl"): + if obj_pid in mtl: + mtl_path = os.path.join(compact_obj_pid_out_dir,mtl) + os.remove(mtl_path) + + mtl_name = None + tex_name = None + for file_name in os.listdir(original_obj_pid_dir): + if file_name.endswith(".mtl"): + if obj_pid in file_name: + mtl_name = file_name + if file_name.endswith(".jpg"): + if obj_pid in file_name: + tex_name = file_name + if file_name.endswith(".png"): + if obj_pid in file_name: + tex_name = file_name + + for file in os.listdir(original_obj_pid_dir): + #print(f"file{file}") + if file.endswith(".obj"): + continue + if obj_pid not in file: + continue + + origin_path = os.path.join(original_obj_pid_dir,file) + dis_path = os.path.join(compact_obj_pid_out_dir,file) + + if os.path.isfile(origin_path): + #print(f'origin_path{origin_path}') + #print(f'dis_path{dis_path}') + shutil.copy(origin_path,dis_path) + time.sleep(1) + #print("-"*50) + base_origin_obj_path = os.path.join(original_obj_pid_dir,obj_name) + #print(f"base_origin_obj_path{base_origin_obj_path}") + #print(f"obj_path_compact{obj_path_compact}") + update_obj_file(base_origin_obj_path, obj_path_compact) + + placed_remove_obj_path = os.path.join(placed_remove_dir, obj_name) + shutil.copy(base_origin_obj_path,placed_remove_obj_path) + os.remove(base_origin_obj_path) + + exist_obj_any = False + exist_obj = False + delete_mtl = False + for file_name in os.listdir(original_obj_pid_dir): + if file_name.endswith(".obj"): + if obj_pid in file_name: + exist_obj = True + exist_obj_any = True + + if not exist_obj_any: + delete_mtl = True + if not exist_obj: + delete_mtl = True + + if delete_mtl: + print("delete_mtl",mtl_name,tex_name) + if mtl_name!=None: + base_origin_mtl_path = os.path.join(original_obj_pid_dir,mtl_name) + placed_remove_mtl_path = os.path.join(placed_remove_dir, mtl_name) + shutil.copy(base_origin_mtl_path,placed_remove_mtl_path) + os.remove(base_origin_mtl_path) + + if tex_name!=None: + base_origin_tex_path = os.path.join(original_obj_pid_dir,tex_name) + placed_remove_tex_path = os.path.join(placed_remove_dir, tex_name) + shutil.copy(base_origin_tex_path,placed_remove_tex_path) + os.remove(base_origin_tex_path) + + print(f"排版错误模型数量::{len(dict_bad)}") + for obj_name in dict_bad: + print("--错误模型名:", obj_name) + process_obj_files(original_obj_pid_dir,bad_dir,obj_name) + + print(f"排版剩余模型数量::{len(dict_unplaced)}") + for ply_file_name in dict_unplaced: + obj_name = ply_file_name.split("=")[0]+".obj" + print("--剩余模型名:", obj_name) + process_obj_files(original_obj_pid_dir,full_dir,obj_name) + +import json + +def extract_angles(R): + # 提取绕X、Y、Z轴的旋转角度(弧度) + rx_rad = np.arctan2(R[2, 1], R[2, 2]) # X轴旋转 + ry_rad = np.arcsin(-R[2, 0]) # Y轴旋转 + rz_rad = np.arctan2(R[1, 0], R[0, 0]) # Z轴旋转 + + # 将弧度转换为角度(度数) + rx_deg = np.degrees(rx_rad) + ry_deg = np.degrees(ry_rad) + rz_deg = np.degrees(rz_rad) + + return rx_deg, ry_deg, rz_deg + +def compute_mesh_center(vertices): + """ + 计算网格质心 + + 参数: + vertices: 顶点坐标数组,形状为(N, 3)的NumPy数组或列表 + + 返回: + centroid: 质心坐标的NumPy数组 [x, y, z] + """ + if len(vertices) == 0: + raise ValueError("顶点数组不能为空") + + n = len(vertices) # 顶点数量 + # 初始化坐标累加器 + sum_x, sum_y, sum_z = 0.0, 0.0, 0.0 + + # 遍历所有顶点累加坐标值 + for vertex in vertices: + sum_x += vertex[0] + sum_y += vertex[1] + sum_z += vertex[2] + + # 计算各坐标轴的平均值 + centroid = np.array([sum_x / n, sum_y / n, sum_z / n]) + return centroid + +import re +def extract_numbers_from_filename(filename): + """ + 从文件名中提取893333, 338908, 105043和x后面的数字 + """ + # 提取前两个下划线前的数字 + first_part = re.findall(r'^(\d+)_(\d+)', filename) + if first_part: + num1, num2 = first_part[0] + else: + num1, num2 = None, None + + # 提取P后面的数字 + p_number = re.findall(r'P(\d+)', filename) + num3 = p_number[0] if p_number else None + + # 提取x后面的数字 + x_number = re.findall(r'x(\d+)', filename) + num4 = x_number[0] if x_number else None + + return [num for num in [num1, num2, num3, num4] if num is not None] + +import requests +def move_obj_to_compact_bounds_json(base_original_obj_dir,dict_mesh_obj,dict_unplaced,dict_bad,bad_dir,full_dir,dict_bounds_fix, + dict_total_matrix,save_mesh,batch_id, print_start_time,selected_machine,selected_mode,version): + """生成3D打印布局的JSON数据并保存为3DPrintLayout.json""" + # 创建符合3DPrintLayout规范的JSON数据结构 + layout_data = { + "summary": { + "version": version, + "homo_matrix": "Homogeneous Matrix", + "precision": 6, + "selected_machine": selected_machine, + "selected_mode": selected_mode + }, + "models": [] + } + + send_layout_data={ + "data": [], + "pre_complate_time": 0.0, + "pre_batch_id": batch_id, + "type_setting_start_time": print_start_time + } + + print("is_test=", is_test) + print(f"need_upload_result={need_upload_result()}") + # if need_upload_result(): + if is_test: + is_send_layout_data = False + else: + is_send_layout_data = True + # is_send_layout_data = False + + obj_file_list = list(dict_mesh_obj.keys()) + ply_path_dict = {} + + original_obj_pid_dir = base_original_obj_dir + + # 构建PLY文件路径映射 + for ply_file_name in dict_bounds_fix: + ply_dict_key = ply_file_name.split("=")[0] + ply_path_dict[ply_dict_key] = ply_file_name + + for obj_name in obj_file_list: + ply_name_pid = obj_name.replace(".obj", "") + ply_name = ply_path_dict.get(ply_name_pid, None) + + if is_send_layout_data: + result = extract_numbers_from_filename(ply_name) + + if not ply_name or ply_name in dict_unplaced: + + if is_send_layout_data: + print_id = result[2] + order_id = result[0] + status = 0 + pid = result[1] + counts = result[3] + send_layout_data["data"].append({ + "print_id": print_id, + "order_id": order_id, + "status": status, + "pid":pid, + "counts":counts}) + + continue # 跳过未放置的模型 + + total_matrix = dict_total_matrix[obj_name] + + flattened = total_matrix.flatten()[:16] + + matrix_4x4 = [ + [round(flattened[i], 6) for i in range(0, 4)], # 第1行 + [round(flattened[i], 6) for i in range(4, 8)], # 第2行 + [round(flattened[i], 6) for i in range(8, 12)], # 第3行 + [round(flattened[i], 6) for i in range(12, 16)] # 第4行 + ] + + layout_data["models"].append({ + "file_name": obj_name, + "transform": { + "homo_matrix": matrix_4x4 + } + }) + + if is_send_layout_data: + print_id = result[2] + order_id = result[0] + status = 1 + pid = result[1] + counts = result[3] + send_layout_data["data"].append({ + "print_id": print_id, + "order_id": order_id, + "status": status, + "pid":pid, + "counts":counts}) + + # 保存JSON文件 + # json_path = os.path.join(base_original_obj_dir, "3DPrintLayout.json") + json_path = os.path.join(base_original_obj_dir, f"{batch_id}.json") + + import re + json_str = json.dumps(layout_data, ensure_ascii=False, indent=2) + json_str = re.sub( + r'\[\s*(-?[\d.]+),\s+(-?[\d.]+),\s+(-?[\d.]+),\s+(-?[\d.]+)\s*\]', + r'[\1,\2,\3,\4]', + json_str + ) + with open(json_path, "w", encoding='utf-8') as f: + f.write(json_str) + + print(f"3D打印布局已保存至: {json_path}") + + print(f"排版错误模型数量::{len(dict_bad)}") + for obj_name in dict_bad: + print("--错误模型名:", obj_name) + process_obj_files(original_obj_pid_dir,bad_dir,obj_name) + + print(f"排版剩余模型数量::{len(dict_unplaced)}") + for ply_file_name in dict_unplaced: + obj_name = ply_file_name.split("=")[0]+".obj" + print("--剩余模型名:", obj_name) + process_obj_files(original_obj_pid_dir,full_dir,obj_name) + + if save_mesh: + cache_type_setting_dir=f"{base_original_obj_dir}/arrange" + transform_save_o3d(layout_data, original_obj_pid_dir, cache_type_setting_dir) + + return send_layout_data + +def process_obj_files(original_obj_pid_dir,placed_remove_dir,obj_name): + """ + 处理OBJ文件及其相关资源文件的复制、更新和清理 + + 参数: + original_obj_pid_dir: 包含原始OBJ文件的目录 + placed_remove_dir: 用于存放移除文件的目录 + obj_name: 要处理的OBJ文件名 + """ + + base_origin_obj_path = os.path.join(original_obj_pid_dir,obj_name) + + # 从obj_name中提取PID(产品ID) + obj_pid = obj_name.split("_P")[0] + + # 查找相关的MTL和纹理文件 + mtl_name = None + tex_name = None + + for file_name in os.listdir(original_obj_pid_dir): + + if file_name.endswith(".mtl") and obj_pid in file_name: + mtl_name = file_name + + if (file_name.endswith(".jpg") or file_name.endswith(".png")) and obj_pid in file_name: + tex_name = file_name + + # 将原始OBJ文件移动到移除目录 + placed_remove_obj_path = os.path.join(placed_remove_dir, obj_name) + shutil.copy(base_origin_obj_path, placed_remove_obj_path) + os.remove(base_origin_obj_path) + + # 检查目录中是否还有其他OBJ文件 + exist_obj_any = False + exist_obj = False + + for file_name in os.listdir(original_obj_pid_dir): + if file_name.endswith(".obj"): + exist_obj_any = True + if obj_pid in file_name: + exist_obj = True + + # 确定是否需要删除MTL和纹理文件 + delete_mtl = not exist_obj_any or not exist_obj + + # 如果确定要删除,移动MTL和纹理文件 + if delete_mtl: + if mtl_name: + base_origin_mtl_path = os.path.join(original_obj_pid_dir, mtl_name) + placed_remove_mtl_path = os.path.join(placed_remove_dir, mtl_name) + shutil.copy(base_origin_mtl_path, placed_remove_mtl_path) + os.remove(base_origin_mtl_path) + + if tex_name: + base_origin_tex_path = os.path.join(original_obj_pid_dir, tex_name) + placed_remove_tex_path = os.path.join(placed_remove_dir, tex_name) + shutil.copy(base_origin_tex_path, placed_remove_tex_path) + os.remove(base_origin_tex_path) + +def update_obj_file(original_obj_path,compact_obj_path): + """""" + with open(original_obj_path, "r") as f: + lines_original = f.readlines() + mtllib_name = None + mat_name = None + for line in lines_original: + if line.startswith("mtllib"): + mtllib_name = line.split(" ")[1] + elif line.startswith("usemtl"): + mat_name = line.split(" ")[1] + if mtllib_name and mat_name: + break + with open(compact_obj_path, "r") as f: + lines = f.readlines() + new_lines = [] + for line2 in lines: + if line2.startswith("mtllib"): + line2 = f"mtllib {mtllib_name}\n" # 替换为原始 MTL 文件路径 + elif line2.startswith("usemtl"): + line2 = f"usemtl {mat_name}\n" # 替换为原始贴图路径 + new_lines.append(line2) + with open(compact_obj_path, "w") as f: + f.writelines(new_lines) + +def pass_for_min_dis(placed_models, dict_unplaced, dict_bounds_fix): + + pcd_all = [] + name_list = [] + model_list = [] + + for model in placed_models: + # pcd = o3d.io.read_point_cloud(ply_origin_path) + pcd = dict_bounds_fix[model['name']] + pcd_all.append(pcd) + name_list.append(model['name']) + model_list.append(model) + + for idx, pcd in enumerate(pcd_all): + y = model_list[idx]['position'][1] + dx = model_list[idx]['dimensions'][0] + dy = model_list[idx]['dimensions'][1] + # print("pass_for_min_dis", name_list[idx], y, dy) + + delta_y = 20 + # safe_y = y - delta_y + safe_y = y - dy - delta_y + min_y = 0 + if safe_y < min_y: + name = name_list[idx] + print("fail to place (x=0)", name_list[idx], y, dy) + dict_unplaced[name]=name + +if __name__ == '__main__': + out_dir = "/data/datasets_20t/type_setting_test_data/" + weight_fix_out_obj_dir = f"{out_dir}/print_weight_fix_data_obj" + weight_fix_out_ply_dir = f"{out_dir}/data/datasets_20t/type_ssetting_test_data/print_weight_fix_data_ply" + + base_original_obj_dir=f"{print_factory_type_dir}/8/" + if not os.path.exists(weight_fix_out_ply_dir): + os.makedirs(weight_fix_out_ply_dir) + bounds_fix_out_dir = f"{out_dir}/print_bounds_fix_data" + bounds_compact_out_dir = f"{out_dir}/print_bounds_compact_data" + compact_obj_out_dir = f"{out_dir}//print_compact_obj" + if not os.path.exists(bounds_fix_out_dir): + os.mkdir(bounds_fix_out_dir) + if not os.path.exists(bounds_compact_out_dir): + os.makedirs(bounds_compact_out_dir) + if not os.path.exists(compact_obj_out_dir): + os.makedirs(compact_obj_out_dir) + + move_obj_to_compact_bounds(bounds_fix_out_dir,bounds_compact_out_dir,weight_fix_out_obj_dir,base_original_obj_dir,compact_obj_out_dir) \ No newline at end of file diff --git a/print_factory_type_setting_obj_run.py b/print_factory_type_setting_obj_run.py index 9e415bc..3bfd037 100644 --- a/print_factory_type_setting_obj_run.py +++ b/print_factory_type_setting_obj_run.py @@ -7,12 +7,21 @@ import argparse script_dir = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, script_dir) -from print_show_weight_max_obj import make_bbox_for_print,copy_obj_2x -from print_mplot3d_point_cloud_layout import * -from print_merged_many_obj import move_compact_obj_to_file +from print_show_weight_max_obj import copy_obj_2x +from point_cloud_layout import * from test_load_json import load_show_save from download_print import upload_result +from config import print_factory_type_dir +from config import is_test +from config import url_send_layout +from config import big_machine_size +from config import small_machine_size + +from general import need_upload_result + +from point_cloud_layout import make_bbox_for_print + def get_base_directory(): """获取脚本或可执行文件的基础目录""" if getattr(sys, 'frozen', False): @@ -26,116 +35,63 @@ def get_base_directory(): from datetime import datetime import gc -def print_type_setting_obj(base_original_obj_dir=None,cache_type_setting_dir=None,batch_id=0,show_chart=True,selected_mode="标准",output_format="JSON",selected_machine="大机型"): +def print_type_setting_obj(base_original_obj_dir=None,batch_id=0,selected_mode="标准",output_format="JSON",selected_machine="大机型"): print_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - weight_fix_out_obj_dir = f"{cache_type_setting_dir}/temp/print_weight_fix_data_obj" - weight_fix_out_ply_dir = f"{cache_type_setting_dir}/temp/print_weight_fix_data_ply" - bounds_fix_out_dir = f"{cache_type_setting_dir}/temp/print_bounds_fix_data" - bounds_compact_out_dir = f"{cache_type_setting_dir}/temp/print_bounds_compact_data" - compact_obj_out_dir = f"{cache_type_setting_dir}/temp/print_compact_obj" # 最后的结果 - placed_remove_dir = f"{base_original_obj_dir}/place_remove_dir" # 已经放置的放到这个目录 - # 获取基础目录 base_path = get_base_directory() # 获取父目录 parent_dir = os.path.dirname(base_path) bad_dir = os.path.join(parent_dir, "bad") full_dir = os.path.join(parent_dir, "full") - # print(bad_dir) - # print(full_dir) - - if output_format == "模型": - if os.path.exists(weight_fix_out_obj_dir): - shutil.rmtree(weight_fix_out_obj_dir) - if os.path.exists(weight_fix_out_ply_dir): - shutil.rmtree(weight_fix_out_ply_dir) - if os.path.exists(bounds_fix_out_dir): - shutil.rmtree(bounds_fix_out_dir) - if os.path.exists(bounds_compact_out_dir): - shutil.rmtree(bounds_compact_out_dir) - if os.path.exists(compact_obj_out_dir): - shutil.rmtree(compact_obj_out_dir) - # if os.path.exists(output_folder): # 不需要 - # shutil.rmtree(output_folder) time.sleep(1) - if not os.path.exists(weight_fix_out_obj_dir): - os.makedirs(weight_fix_out_obj_dir) - if not os.path.exists(weight_fix_out_ply_dir): - os.makedirs(weight_fix_out_ply_dir) - if not os.path.exists(bounds_fix_out_dir): - os.mkdir(bounds_fix_out_dir) - if not os.path.exists(bounds_compact_out_dir): - os.makedirs(bounds_compact_out_dir) - if not os.path.exists(compact_obj_out_dir): - os.makedirs(compact_obj_out_dir) - # if not os.path.exists(output_folder): # 不需要 - # os.makedirs(output_folder) print("selected_machine",selected_machine,"selected_mode",selected_mode,"output_format",output_format) compact_min_dis = True - compact_min_dis2 = False if selected_mode=="标准" : compact_min_dis = False - # if output_format=="JSON": - compact_min_dis2 = True else : compact_min_dis = True move_back = True - machine_size = [600, 500, 300] + machine_size = big_machine_size if selected_machine=="小机型": - machine_size[0] = 380 - machine_size[1] = 345 - machine_size[2] = 250 + machine_size = small_machine_size start_time = time.time() copy_obj_2x(base_original_obj_dir) dict_bad = {} - dict_best_angel = {} - dict_fix = {} dict_origin = {} - dict_origin_real = {} - dict_total_matrix= {} - dict_mesh_obj = make_bbox_for_print(base_original_obj_dir, weight_fix_out_obj_dir, weight_fix_out_ply_dir,show_chart,dict_bad, dict_best_angel,dict_fix,dict_origin,dict_origin_real,compact_min_dis or compact_min_dis2,dict_total_matrix) - mesh_count = len(dict_mesh_obj) + + dict_total_matrix, all_models, dict_pcd_fix = make_bbox_for_print(base_original_obj_dir,dict_bad,dict_origin,compact_min_dis) + mesh_count = len(dict_origin) if mesh_count<=0: print("选择的文件夹没有模型") return -1 + end_time1 = time.time() - dict_bounds_fix = {} - placed_models= ply_print_layout_platform(weight_fix_out_obj_dir,weight_fix_out_ply_dir,bounds_fix_out_dir,show_chart,dict_mesh_obj,dict_fix,dict_bounds_fix,machine_size,dict_total_matrix) + + ###以上迁移到部分前置计算 + + dict_pcd_fix2 = {} + list_placed_model = ply_print_layout_platform(dict_pcd_fix,dict_pcd_fix2,machine_size,dict_total_matrix,all_models) + end_time2 = time.time() + dict_unplaced = {} - dict_compact = {} + + dict_mesh_obj = dict_origin if compact_min_dis: - # if output_format=="JSON" : - if True : - can_compact_json = True - if can_compact_json : - compact_mode_for_min_dis1_json(bounds_fix_out_dir,bounds_compact_out_dir,show_chart,move_back,placed_models,dict_unplaced,dict_bounds_fix,dict_compact,machine_size,dict_total_matrix) - else : - pass_for_min_dis(bounds_fix_out_dir, bounds_compact_out_dir,placed_models, dict_unplaced,dict_bounds_fix,dict_compact) - else : - compact_mode_for_min_dis(bounds_fix_out_dir,bounds_compact_out_dir,show_chart,move_back,placed_models,dict_unplaced,dict_bounds_fix,dict_compact,machine_size) + compact_mode_for_min_dis_json(list_placed_model,dict_unplaced,dict_pcd_fix,machine_size,dict_total_matrix) else: - compact_min_dis2 = False - if compact_min_dis2: - compact_mode_for_min_dis2_json(bounds_fix_out_dir,bounds_compact_out_dir,show_chart,move_back,placed_models,dict_unplaced,dict_bounds_fix,dict_compact,machine_size,dict_total_matrix) - else : - pass_for_min_dis(bounds_fix_out_dir, bounds_compact_out_dir,placed_models, dict_unplaced,dict_bounds_fix,dict_compact) + pass_for_min_dis(list_placed_model, dict_unplaced,dict_pcd_fix) end_time3 = time.time() - #if os.path.exists(placed_remove_dir): - # shutil.rmtree(placed_remove_dir) - if not os.path.exists(placed_remove_dir): - os.makedirs(placed_remove_dir) - if not os.path.exists(bad_dir): os.makedirs(bad_dir) @@ -143,23 +99,14 @@ def print_type_setting_obj(base_original_obj_dir=None,cache_type_setting_dir=Non os.makedirs(full_dir) is_small_machine = True if selected_machine=="小机型" else False - use_json = True if output_format=="JSON" else False save_mesh = True if output_format=="模型" else False - # if use_json: version = "print_type_setting25.local" - layout_data, send_layout_data = move_obj_to_compact_bounds_json(bounds_fix_out_dir,bounds_compact_out_dir,weight_fix_out_obj_dir, - base_original_obj_dir,compact_obj_out_dir,dict_mesh_obj,dict_unplaced, - placed_remove_dir,dict_bad,bad_dir,full_dir,dict_best_angel,dict_bounds_fix, - dict_compact,dict_origin,dict_total_matrix,save_mesh,cache_type_setting_dir, - batch_id, print_start_time,selected_machine,selected_mode,version) - - # else: - # move_obj_to_compact_bounds(bounds_fix_out_dir,bounds_compact_out_dir,weight_fix_out_obj_dir,base_original_obj_dir,compact_obj_out_dir,dict_mesh_obj,dict_unplaced,placed_remove_dir,dict_bad,bad_dir,full_dir,dict_bounds_fix,dict_compact,dict_origin) + send_layout_data = move_obj_to_compact_bounds_json(base_original_obj_dir,dict_mesh_obj,dict_unplaced,dict_bad,bad_dir,full_dir,dict_pcd_fix, + dict_total_matrix,save_mesh,batch_id,print_start_time,selected_machine,selected_mode,version) end_time4 = time.time() - #move_compact_obj_to_file(compact_obj_out_dir, output_folder) # 不需要 print("排版完成") end_time = time.time() elapsed_seconds = end_time - start_time @@ -179,26 +126,12 @@ def print_type_setting_obj(base_original_obj_dir=None,cache_type_setting_dir=Non print(f"挪紧凑::{elapsed_minutes3} 分 / {elapsed_seconds3} 秒") print(f"移动到位置::{elapsed_minutes4} 分 / {elapsed_seconds4} 秒") - dict_mesh_obj.clear() - del dict_mesh_obj - dict_bad.clear() - del dict_bad - dict_fix.clear() - del dict_fix - dict_bounds_fix.clear() - del dict_bounds_fix - dict_unplaced.clear() - del dict_unplaced - dict_compact.clear() - del dict_compact - gc.collect() - # print(base_original_obj_dir,blank_dir,batch_id) is_screenshot = True if is_screenshot: start_time = time.time() - blank_path = get_blank_path(parent_dir, is_small_machine) + blank_path = get_blank_path(is_small_machine) load_show_save(base_original_obj_dir, dict_origin, blank_path, batch_id) elapsed_seconds5 = time.time() - start_time elapsed_minutes5 = int(elapsed_seconds5 // 60) @@ -210,23 +143,24 @@ def print_type_setting_obj(base_original_obj_dir=None,cache_type_setting_dir=Non is_upload_result = True if is_upload_result: - print(f"执行上传-parent_dir={parent_dir},base_original_obj_dir={base_original_obj_dir},batch_id={batch_id}") + print(f"执行上 传-parent_dir={print_factory_type_dir},base_original_obj_dir={base_original_obj_dir},batch_id={batch_id}") # oss_config = f"{base_original_obj_dir}/../print_factory_type_setting_big/download_print/run.yaml" - oss_config = f"{parent_dir}/print_factory_type_setting_big/download_print/run.yaml" + oss_config = f"{print_factory_type_dir}/print_factory_type_setting_big/download_print/run.yaml" upload_result(base_original_obj_dir, oss_config, batch_id) print(f"is_test={is_test}") + print(f"need_upload_result={need_upload_result()}") + # if need_upload_result(): if is_test : is_send_layout_data = False else : is_send_layout_data = True - # is_send_layout_data = False if is_send_layout_data: print(f"send_layout_data={send_layout_data}") - url = 'https://mp.api.suwa3d.com/api/printTypeSettingOrder/printTypeSettingOrderSuccess' - # url = 'http://127.0.0.1:8199/api/typeSettingPrintOrder/printTypeSettingOrderSuccess' + # url = 'https://mp.api.suwa3d.com/api/printTypeSettingOrder/printTypeSettingOrderSuccess' + url = url_send_layout try: response = requests.post(url, json.dumps(send_layout_data), timeout=30) #写入文件中 log/request.txt @@ -254,38 +188,46 @@ def print_type_setting_obj(base_original_obj_dir=None,cache_type_setting_dir=Non return 0 -def get_blank_path(parent_dir=None, is_small_machine=False): +def get_blank_path(is_small_machine=False): if is_small_machine: - return os.path.join(parent_dir, "blank/blank_bias/blank_small.obj") + return os.path.join(print_factory_type_dir, "blank/blank_bias/blank_small.obj") else: - return os.path.join(parent_dir, "blank/blank_bias/blank2.obj") + return os.path.join(print_factory_type_dir, "blank/blank_bias/blank2.obj") def preview(base_original_obj_dir=None, batch_id=0, is_small_machine=False): base_path = get_base_directory() parent_dir = os.path.dirname(base_path) # blank_dir = os.path.join(parent_dir, "blank", "blank_bias") - blank_path = get_blank_path(parent_dir, is_small_machine) + blank_path = get_blank_path(is_small_machine) load_show_save(base_original_obj_dir, {}, blank_path, batch_id, True) -if __name__ == '__main__': +def get_pcd(obj, is_down_sample=True): + vertices = np.asarray(obj.vertices) + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(vertices) + + if is_down_sample: + voxel_size = 3 + pcd_downsampled = down_sample(pcd, voxel_size, False) + return pcd_downsampled + else: + return pcd +if __name__ == '__main__': # parser = argparse.ArgumentParser() # parser.add_argument("--batch_id", type=str, required=True, help="batch_id") # args = parser.parse_args() # batch_id = args.batch_id - batch_id = "9" + batch_id = "1" src_dir = batch_id selected_mode="紧凑" # 标准 紧凑 output_format="JSON" # 模型 JSON selected_machine = "大机型" # 小机型 大机型 - print_factory_type_dir="/root/print_factory_type" - # cache_type_setting_dir=f"/data/datasets_20t/type_setting_test_data/{src_dir}" - cache_type_setting_dir=f"{print_factory_type_dir}/{src_dir}/arrange" base_original_obj_dir = f"{print_factory_type_dir}/{src_dir}" - print_type_setting_obj(base_original_obj_dir=base_original_obj_dir,cache_type_setting_dir=cache_type_setting_dir, - batch_id=batch_id,show_chart=False,selected_mode=selected_mode,output_format=output_format,selected_machine=selected_machine) + print_type_setting_obj(base_original_obj_dir=base_original_obj_dir, + batch_id=batch_id,selected_mode=selected_mode,output_format=output_format,selected_machine=selected_machine) diff --git a/print_factory_type_setting_obj_run_GUI.py b/print_factory_type_setting_obj_run_GUI.py deleted file mode 100644 index d480010..0000000 --- a/print_factory_type_setting_obj_run_GUI.py +++ /dev/null @@ -1,193 +0,0 @@ -import os -import shutil -import time -import sys - -script_dir = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, script_dir) - -from print_show_weight_max_obj import make_bbox_for_print,copy_obj_2x -from print_mplot3d_point_cloud_layout import * -from print_merged_many_obj import move_compact_obj_to_file -from test_load_json import load_and_show - - -def get_base_directory(): - """获取脚本或可执行文件的基础目录""" - if getattr(sys, 'frozen', False): - # 打包后的可执行文件环境 - base_path = os.path.dirname(sys.executable) - else: - # 正常脚本运行环境 - base_path = os.path.dirname(os.path.abspath(__file__)) - return base_path - -from datetime import datetime -def print_type_setting_obj(base_original_obj_dir=None,cache_type_setting_dir=None,show_chart=True,selected_mode="标准",output_format="JSON",selected_machine="大机型"): - - print_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - weight_fix_out_obj_dir = f"{cache_type_setting_dir}/print_weight_fix_data_obj" - weight_fix_out_ply_dir = f"{cache_type_setting_dir}/print_weight_fix_data_ply" - bounds_fix_out_dir = f"{cache_type_setting_dir}/print_bounds_fix_data" - bounds_compact_out_dir = f"{cache_type_setting_dir}/print_bounds_compact_data" - compact_obj_out_dir = f"{cache_type_setting_dir}/print_compact_obj" # 最后的结果 - placed_remove_dir = f"{base_original_obj_dir}/place_remove_dir" # 已经放置的放到这个目录 - - # 获取基础目录 - base_path = get_base_directory() - # 获取父目录 - parent_dir = os.path.dirname(base_path) - bad_dir = os.path.join(parent_dir, "bad") - full_dir = os.path.join(parent_dir, "full") - blank_dir = os.path.join(parent_dir, "blank") - # print(bad_dir) - # print(full_dir) - - # 测试代码 - """ - selected_machine = "小机型" # 小机型 大机型 - selected_mode="紧凑" # 标准 紧凑 - output_format="模型" # 模型 JSON - """ - - if output_format == "模型": - if os.path.exists(weight_fix_out_obj_dir): - shutil.rmtree(weight_fix_out_obj_dir) - if os.path.exists(weight_fix_out_ply_dir): - shutil.rmtree(weight_fix_out_ply_dir) - if os.path.exists(bounds_fix_out_dir): - shutil.rmtree(bounds_fix_out_dir) - if os.path.exists(bounds_compact_out_dir): - shutil.rmtree(bounds_compact_out_dir) - if os.path.exists(compact_obj_out_dir): - shutil.rmtree(compact_obj_out_dir) - # if os.path.exists(output_folder): # 不需要 - # shutil.rmtree(output_folder) - - time.sleep(1) - if not os.path.exists(weight_fix_out_obj_dir): - os.makedirs(weight_fix_out_obj_dir) - if not os.path.exists(weight_fix_out_ply_dir): - os.makedirs(weight_fix_out_ply_dir) - if not os.path.exists(bounds_fix_out_dir): - os.mkdir(bounds_fix_out_dir) - if not os.path.exists(bounds_compact_out_dir): - os.makedirs(bounds_compact_out_dir) - if not os.path.exists(compact_obj_out_dir): - os.makedirs(compact_obj_out_dir) - # if not os.path.exists(output_folder): # 不需要 - # os.makedirs(output_folder) - - print("selected_machine",selected_machine,"selected_mode",selected_mode,"output_format",output_format) - compact_min_dis = True - compact_min_dis2 = False - if selected_mode=="标准" : - compact_min_dis = False - if output_format=="JSON": - compact_min_dis2 = True - else : - compact_min_dis = True - - move_back = True - - machine_size = [600, 500, 300] - if selected_machine=="小机型": - machine_size[0] = 380 - machine_size[1] = 345 - machine_size[2] = 250 - - start_time = time.time() - copy_obj_2x(base_original_obj_dir) - dict_bad = {} - dict_best_angel = {} - dict_fix = {} - dict_origin = {} - dict_total_matrix= {} - dict_mesh_obj = make_bbox_for_print(base_original_obj_dir, weight_fix_out_obj_dir, weight_fix_out_ply_dir,show_chart,dict_bad, dict_best_angel,dict_fix,dict_origin,compact_min_dis or compact_min_dis2,dict_total_matrix) - mesh_count = len(dict_mesh_obj) - if mesh_count<=0: - print("选择的文件夹没有模型") - return -1 - end_time1 = time.time() - dict_bounds_fix = {} - placed_models= ply_print_layout_platform(weight_fix_out_obj_dir,weight_fix_out_ply_dir,bounds_fix_out_dir,show_chart,dict_mesh_obj,dict_fix,dict_bounds_fix,machine_size,dict_total_matrix) - end_time2 = time.time() - dict_unplaced = {} - dict_compact = {} - - if compact_min_dis: - if output_format=="JSON" : - can_compact_json = True - if can_compact_json : - compact_mode_for_min_dis1_json(bounds_fix_out_dir,bounds_compact_out_dir,show_chart,move_back,placed_models,dict_unplaced,dict_bounds_fix,dict_compact,machine_size,dict_total_matrix) - else : - pass_for_min_dis(bounds_fix_out_dir, bounds_compact_out_dir,placed_models, dict_unplaced,dict_bounds_fix,dict_compact) - else : - compact_mode_for_min_dis(bounds_fix_out_dir,bounds_compact_out_dir,show_chart,move_back,placed_models,dict_unplaced,dict_bounds_fix,dict_compact,machine_size) - else: - if compact_min_dis2: - compact_mode_for_min_dis2_json(bounds_fix_out_dir,bounds_compact_out_dir,show_chart,move_back,placed_models,dict_unplaced,dict_bounds_fix,dict_compact,machine_size,dict_total_matrix) - else : - pass_for_min_dis(bounds_fix_out_dir, bounds_compact_out_dir,placed_models, dict_unplaced,dict_bounds_fix,dict_compact) - - end_time3 = time.time() - - #if os.path.exists(placed_remove_dir): - # shutil.rmtree(placed_remove_dir) - if not os.path.exists(placed_remove_dir): - os.makedirs(placed_remove_dir) - - if not os.path.exists(bad_dir): - os.makedirs(bad_dir) - - if not os.path.exists(full_dir): - os.makedirs(full_dir) - - save_mesh = True if output_format=="模型" else False - # move_obj_to_compact_bounds_json(bounds_fix_out_dir,bounds_compact_out_dir,weight_fix_out_obj_dir,base_original_obj_dir,compact_obj_out_dir,dict_mesh_obj,dict_unplaced,placed_remove_dir,dict_bad,bad_dir,full_dir,dict_best_angel,dict_bounds_fix,dict_compact,save_mesh,dict_origin,dict_total_matrix,print_start_time) - - version = "print_type_setting25.11.21.1" - batch_id = os.path.basename(base_path.rstrip('/')) - move_obj_to_compact_bounds_json(bounds_fix_out_dir,bounds_compact_out_dir,weight_fix_out_obj_dir, - base_original_obj_dir,compact_obj_out_dir,dict_mesh_obj,dict_unplaced, - placed_remove_dir,dict_bad,bad_dir,full_dir,dict_best_angel,dict_bounds_fix, - dict_compact,dict_origin,dict_total_matrix,save_mesh,cache_type_setting_dir, - batch_id, print_start_time,selected_machine,selected_mode,version) - end_time4 = time.time() - #move_compact_obj_to_file(compact_obj_out_dir, output_folder) # 不需要 - print("排版完成") - end_time = time.time() - elapsed_seconds = end_time - start_time - elapsed_seconds1 = end_time1 - start_time # 计算重心 - elapsed_seconds2 = end_time2 - end_time1 # - elapsed_seconds3 = end_time3 - end_time2 - elapsed_seconds4 = end_time4 - end_time3 - elapsed_minutes = int(elapsed_seconds // 60) - elapsed_minutes1 = int(elapsed_seconds1 // 60) - elapsed_minutes2 = int(elapsed_seconds2 // 60) - elapsed_minutes3 = int(elapsed_seconds3 // 60) - elapsed_minutes4 = int(elapsed_seconds4 // 60) - print(f"排版总耗时::{elapsed_minutes} 分 / {elapsed_seconds} 秒") - print(f"计算重心::{elapsed_minutes1} 分 / {elapsed_seconds1} 秒") - print(f"排包围盒::{elapsed_minutes2} 分 / {elapsed_seconds2} 秒") - - print(f"挪紧凑::{elapsed_minutes3} 分 / {elapsed_seconds3} 秒") - print(f"移动到位置::{elapsed_minutes4} 分 / {elapsed_seconds4} 秒") - - # load_and_show(base_original_obj_dir,blank_dir) - - return 0 - -def preview(base_original_obj_dir=None): - - base_path = get_base_directory() - parent_dir = os.path.dirname(base_path) - blank_dir = os.path.join(parent_dir, "blank", "blank_bias") - - load_and_show(base_original_obj_dir,blank_dir) - -if __name__ == '__main__': - src_dir = "12-9" # 1 5.6.5 5.6.4 5.6.1 5.9 temp - cache_type_setting_dir=f"/data/datasets_20t/type_setting_test_data/{src_dir}" - base_original_obj_dir = f"{print_factory_type_dir}/{src_dir}" - print_type_setting_obj(base_original_obj_dir=base_original_obj_dir,cache_type_setting_dir=cache_type_setting_dir,show_chart=False) diff --git a/print_merged_many_obj.py b/print_merged_many_obj.py deleted file mode 100644 index 1671fb0..0000000 --- a/print_merged_many_obj.py +++ /dev/null @@ -1,162 +0,0 @@ -import os -import shutil -import time - -def merged_obj_for_group(input_folder,output_folder): - """""" - group_size = 5 - obj_pid_list = os.listdir(input_folder) - group_obj_list = [obj_pid_list[i:i + group_size] for i in range(0, len(obj_pid_list), group_size)] - print(group_obj_list) - for group_obj in group_obj_list: - print(group_obj) - group_pid = "_".join(group_obj) - print(group_pid) - - output_group_folder = os.path.join(output_folder,group_pid) - os.makedirs(output_group_folder, exist_ok=True) - - #input_root_folder = "/data/datasets_20t/obj_merger_test_data/" - #output_folder = "/data/datasets_20t/obj_merger_result/" - output_obj = os.path.join(output_group_folder, f"{group_pid}.obj") - output_mtl = os.path.join(output_group_folder, f"{group_pid}.mtl") - - # 初始化 - merged_obj = [] - merged_mtl = [] - texture_files = set() - material_offset = 0 - vertex_offset = 0 - texture_offset = 0 - normal_offset = 0 - current_materials = {} - - # 遍历每个子文件夹 - for folder_name in group_obj: - folder_path = os.path.join(input_folder, folder_name) - if not os.path.isdir(folder_path): - continue - - obj_file = None - mtl_file = None - texture_file = None - - # 寻找 .obj、.mtl 和 .jpg 文件 - for file_name in os.listdir(folder_path): - if file_name.endswith(".obj"): - obj_file = os.path.join(folder_path, file_name) - elif file_name.endswith(".mtl"): - mtl_file = os.path.join(folder_path, file_name) - elif file_name.endswith(".jpg"): - texture_file = os.path.join(folder_path, file_name) - - # 跳过不完整的文件夹 - if not obj_file or not mtl_file or not texture_file: - print(f"跳过不完整的文件夹:{folder_path}") - continue - - # 读取 .obj 文件 - with open(obj_file, "r") as obj_f: - obj_lines = obj_f.readlines() - - - for line in obj_lines: - if line.startswith("mtllib"): - # 替换材质文件名 - - merged_obj.append(f"mtllib {os.path.basename(output_mtl)}\n") - - elif line.startswith("usemtl"): - # 重命名材质名称,避免冲突 - original_material = line.split()[1] - new_material = f"{original_material}_{material_offset}" - print(f"original_material---{original_material}") - print(f"new_material---{new_material}") - merged_obj.append(f"usemtl {new_material}\n") - current_materials[original_material] = new_material - elif line.startswith("v "): # 顶点 - vertex = line.split()[1:] - merged_obj.append(f"v {' '.join(vertex)}\n") - elif line.startswith("vt "): # 纹理坐标 - texture = line.split()[1:] - merged_obj.append(f"vt {' '.join(texture)}\n") - elif line.startswith("vn "): # 法线 - normal = line.split()[1:] - merged_obj.append(f"vn {' '.join(normal)}\n") - elif line.startswith("f "): # 面数据 - face = line.split()[1:] - updated_face = [] - for vertex in face: - indices = vertex.split("/") - indices = [ - str(int(indices[0]) + vertex_offset) if indices[0] else "", - str(int(indices[1]) + texture_offset) if len(indices) > 1 and indices[1] else "", - str(int(indices[2]) + normal_offset) if len(indices) > 2 and indices[2] else "", - ] - updated_face.append("/".join(indices)) - merged_obj.append(f"f {' '.join(updated_face)}\n") - - # 更新偏移量 - vertex_offset += sum(1 for line in obj_lines if line.startswith("v ")) - texture_offset += sum(1 for line in obj_lines if line.startswith("vt ")) - normal_offset += sum(1 for line in obj_lines if line.startswith("vn ")) - - # 读取 .mtl 文件 - with open(mtl_file, "r") as mtl_f: - mtl_lines = mtl_f.readlines() - - for line in mtl_lines: - if line.startswith("newmtl"): - # 重命名材质 - original_material = line.split()[1] - new_material = current_materials.get(original_material, original_material) - merged_mtl.append(f"newmtl {new_material}\n") - elif line.startswith(("map_Kd", "map_Ka", "map_bump")): - # 替换贴图路径为相对路径 - texture_name = os.path.basename(texture_file) - merged_mtl.append(f"{line.split()[0]} {texture_name}\n") - texture_files.add(texture_file) - else: - merged_mtl.append(line) - - material_offset += 1 - - # 写入合并后的 .obj 和 .mtl 文件 - with open(output_obj, "w") as obj_out: - obj_out.writelines(merged_obj) - - with open(output_mtl, "w") as mtl_out: - mtl_out.writelines(merged_mtl) - print(f"texture_files====={texture_files}") - # 将纹理文件复制到输出目录 - for texture_file in texture_files: - shutil.copy(texture_file, output_group_folder) - - print(f"合并完成:{output_obj} 和 {output_mtl}") - print(f"纹理文件已复制到:{output_group_folder}") - -def move_compact_obj_to_file(input_folder,output_folder): - """""" - group_size = 50 - obj_pid_list = os.listdir(input_folder) - group_obj_list = [obj_pid_list[i:i + group_size] for i in range(0, len(obj_pid_list), group_size)] - print(group_obj_list) - for group_obj in group_obj_list: - print(f"group_obj{group_obj}") - out_obj_file_name = group_obj[0]+"_"+group_obj[-1] - print(f"out_obj_file_name:::{out_obj_file_name}") - group_out_put_dir = os.path.join(output_folder,out_obj_file_name) - os.makedirs(group_out_put_dir,exist_ok=True) - for obj_name in group_obj: - original_obj_dir = os.path.join(input_folder,obj_name) - for file_name in os.listdir(original_obj_dir): - original_path = os.path.join(original_obj_dir,file_name) - dis_path = os.path.join(group_out_put_dir,file_name) - shutil.copy(original_path,dis_path) - print("分组完成。") - -if __name__ == '__main__': - input_folder = "/data/datasets_20t/type_setting_test_data/print_compact_obj/" - output_folder = "/data/datasets_20t/type_setting_test_data/obj_merger_result/" - #merged_obj_for_group(input_folder,output_folder) - move_compact_obj_to_file(input_folder, output_folder) diff --git a/print_mplot3d_point_cloud_layout.py b/print_mplot3d_point_cloud_layout.py deleted file mode 100644 index e4bc2f4..0000000 --- a/print_mplot3d_point_cloud_layout.py +++ /dev/null @@ -1,3619 +0,0 @@ -import os -import json -import requests -import shutil -import time -import random -import matplotlib.pyplot as plt -import open3d as o3d -import numpy as np -from grid_near_three import make_near_dict -#import bpy -from plyfile import PlyData, PlyElement -from test_load_json import custom_mesh_transform -from download_print import is_test - -class Platform: - def __init__(self, width, depth, height): - self.width = width - self.depth = depth - self.height = height - self.placed_models = [] # 已放置的模型 - self.unplaced_models = [] # 未能放置的模型 - self.first_line = True - - #""" - def can_place(self, x, y, z, model): - """检查模型是否可以放置在指定位置""" - mx, my, mz = model['dimensions'] - # 检查是否超出平台边界 - # print("can_place1",x + mx,self.width,y + my,self.depth,z + mz,self.height) - if x + mx > self.width or y + my > self.depth or z + mz > self.height: - return False - # 检查是否与已有模型重叠 - for placed in self.placed_models: - px, py, pz = placed['position'] - pdx, pdy, pdz = placed['dimensions'] - # print("can_place2",px, py, pz, pdx, pdy, pdz) - if not ( - x + mx <= px or px + pdx <= x or - y + my <= py or py + pdy <= y or - z + mz <= pz or pz + pdz <= z - ): - return False - return True - - # 原始使用的 - def place_model(self, model): - """尝试将模型放在底面(Z=0)的一层中""" - mx, my, mz = model['dimensions'] - # 如果模型太高,直接跳过 - if mz > self.height: - print("unplaced_models1", model) - self.unplaced_models.append(model) - return False - z = 0 # 固定只放置在底层 - for y in range(0, self.depth - my + 1): - for x in range(0, self.width - mx + 1): - if self.can_place(x, y, z, model): - model['position'] = (x, y, z) - self.placed_models.append(model) - return True - print("unplaced_models2", model) - self.unplaced_models.append(model) - return False - #""" - - """ - import itertools - - def can_place2(self, position, dimensions, placed_items, container_size): - # 三维AABB碰撞检测优化版[6,8](@ref) - # 边界约束检查 - if any(position[i] + dimensions[i] > container_size[i] for i in range(3)): - return False - - # 空间网格加速检测(参考网页3) - grid_size = 50 # 网格划分粒度 - x_min = position[0] // grid_size - x_max = (position[0] + dimensions[0]) // grid_size - y_min = position[1] // grid_size - y_max = (position[1] + dimensions[1]) // grid_size - - # 获取相关网格内的模型[3](@ref) - related_items = [] - for gx in range(int(x_min), int(x_max)+1): - for gy in range(int(y_min), int(y_max)+1): - related_items.extend([item for item in placed_items - if item['grid_x'] == gx and item['grid_y'] == gy]) - - # 精确AABB检测[6](@ref) - for item in related_items: - item_pos = item['position'] - item_dim = item['dimensions'] - overlap_x = (position[0] < item_pos[0] + item_dim[0]) and (position[0] + dimensions[0] > item_pos[0]) - overlap_y = (position[1] < item_pos[1] + item_dim[1]) and (position[1] + dimensions[1] > item_pos[1]) - overlap_z = (position[2] < item_pos[2] + item_dim[2]) and (position[2] + dimensions[2] > item_pos[2]) - if overlap_x and overlap_y and overlap_z: - return False - return True - - def place_model2(self, models, container_size): - # 优化后的装箱主函数[2,5](@ref) - placed = [] - unplaced = [] - grid_size = 50 # 与can_place中保持一致 - - print(type(models[0]['dimensions'])) # 预期输出: - - # 按体积降序排序[5](@ref) - sorted_models = sorted(models, - key=lambda m: m['dimensions'][0]*m['dimensions'][1]*m['dimensions'][2], - reverse=True) - - # 支持6种旋转方向[2](@ref) - rotations = [ - (0,1,2), (0,2,1), (1,0,2), - (1,2,0), (2,0,1), (2,1,0) - ] - - for model in sorted_models: - placed_flag = False - original_dim = model['dimensions'] - - # 尝试所有旋转方向 - for rot in rotations: - rotated_dim = [original_dim[rot[0]], original_dim[rot[1]], original_dim[rot[2]]] - if rotated_dim[2] > container_size[2]: - continue # 跳过高度超标 - - # 优化搜索顺序:从右向左,从下向上[3](@ref) - for y in range(container_size[1] - rotated_dim[1], -1, -1): - for x in range(container_size[0] - rotated_dim[0], -1, -1): - if self.can_place((x,y,0), rotated_dim, placed, container_size): - # 记录网格位置 - model['grid_x'] = x // grid_size - model['grid_y'] = y // grid_size - model['position'] = (x, y, 0) - model['dimensions'] = rotated_dim - placed.append(model) - placed_flag = True - break - if placed_flag: break - if placed_flag: break - - if not placed_flag: - unplaced.append(model) - - return placed, unplaced - #""" - #""" - def can_place3(self, x, y, z, model, is_print=False): - mx, my, mz = model['dimensions'] - - """ - # 边界检查(增加扩展间距) - if (x + mx > self.width or - y + my > self.depth or - z + mz > self.height or - y<=0): - print("can_place3",False) - return False - """ - #print("placed1",x,mx,y,my) - # 边界检查(增加扩展间距) - extend_dist = 4 - if (x - mx < 0 or - y - my < 0 or - z + mz > self.height or - y>=self.depth - extend_dist): - # print("can_place3 1",False, x, mx, y, my, z, mz, self.height) - return False - - # 碰撞检测(正确逻辑与间距处理) - extend_dist_x = 4 # 与place_model3中的扩展距离一致 - extend_dist_y = 2 # 与place_model3中的扩展距离一致 - for placed in self.placed_models: - px, py, pz = placed['position'] - pdx, pdy, pdz = placed['dimensions'] - - """ - # 使用AABB碰撞检测算法[4](@ref) - if (x < px + pdx + extend_dist_x and - x + mx + extend_dist_x > px and - y < py + pdy + extend_dist_y and - y + my + extend_dist_y > py and - z < pz + pdz and - z + mz > pz): - print("can_place3",False) - return False - #""" - - # if is_print: - # print("can_place3",y,py,my,pdy,extend_dist_y) - - #""" - # print("placed2",x,px,pdx,extend_dist_x,mx,self.width) - # 使用AABB碰撞检测算法[4](@ref) - if (x > px - pdx - extend_dist_x and - x - mx - extend_dist_x < px and - y > py - pdy - extend_dist_y and - y - my - extend_dist_y < py and - z < pz + pdz and - z + mz > pz): - # print("can_place3 2",False,model,x,y,z,px,pdx,extend_dist_x,py,pdy,extend_dist_y,my,pz,pdz,pz) - return False - #""" - - # print("can_place3",True) - return True - - def place_model3(self, model, pre_model): - mx, my, mz = model['dimensions'] - if mz > self.height: - self.unplaced_models.append(model) - return False - - z = 0 - extend_dist = 4 - - if pre_model is None: - if self.first_line: - model['position'] = (mx+extend_dist, self.depth - extend_dist, 0) - print(f"First Model {model['name']}") - model['first_line'] = True - else: - model['position'] = (self.width - extend_dist, self.depth - extend_dist, 0) - model['first_line'] = False - - # print("model position1", model['name'], model['position']) - self.placed_models.append(model) - return True - - pre_px, pre_py, pre_pz = pre_model['position'] - pre_mx, pre_my, pre_mz = pre_model['dimensions'] - if self.first_line: - px = pre_px + mx + extend_dist - model['first_line'] = True - else: - px = pre_px - pre_mx - extend_dist - model['first_line'] = False - print(model['name'], "px", px, pre_px, pre_mx) - - """ - if px + mx > self.width: - px = 0 - start_y = self.depth - my - extend_dist - for y in range(start_y, -1, -1): - if self.can_place3(px, y, z, model)==False: - y -= 1 - model['position'] = (px, y, z) - self.placed_models.append(model) - return True - else: - start_y = self.depth - my - extend_dist - for y in range(start_y, -1, -1): - if self.can_place3(px, y, z, model)==False: - y -= 1 - model['position'] = (px, y, z) - self.placed_models.append(model) - return True - """ - reach_limit_x = False - if self.first_line: - if px > self.width: - reach_limit_x = True - else: - if px - mx < 0: - reach_limit_x = True - - if reach_limit_x: - self.first_line = False - px = self.width - extend_dist - # final_y = self.depth - my - extend_dist - final_y = self.depth - print("reach_limit_x final_y1", my, final_y, my, extend_dist, px) - for y in range(my, final_y, +1): - # print("y",y) - if self.can_place3(px, y, z, model, True)==False: - y -= 1 - model['position'] = (px, y, z) - # print("model position2", model['name'], model['position']) - self.placed_models.append(model) - return True - else: - start_y = my + extend_dist - # final_y = self.depth - my - extend_dist - final_y = self.depth - # print("final_y2", start_y, final_y, my, extend_dist, px) - for y in range(start_y, final_y, +1): - if self.can_place3(px, y, z, model)==False: - y -= 1 - model['position'] = (px, y, z) - # print("model position2", model['name'], model['position']) - self.placed_models.append(model) - return True - - # print("model position3", model['name'], model['position']) - self.unplaced_models.append(model) - return False - #""" - def arrange_models(self, models): - # 小打印机380*345, 大打印机600*500*300 - delta = 10 - """ - x_max = -380 + delta - y_max = -345 + delta - """ - # x_length = 500 - delta - # y_length = 300 - delta - """对所有模型进行排布(单层)""" - print("⚠️ 单层放置模式:所有模型只能放在平台底面(Z=0)") - # 按高度和面积排序,优先放大模型 - models = sorted(models, key=lambda m: (-m['dimensions'][2], -m['dimensions'][0] * m['dimensions'][1])) - pre_model = None - for model in models: - # self.place_model(model) - # self.place_model2(model, container) - if self.place_model3(model, pre_model): - pre_model = model - print("arrange_models", model['name']) - - def print_results(self): - """打印排布结果""" - print("Placed Models:") - for model in self.placed_models: - print(f" - {model['name']} at {model['position']} with dimensions {model['dimensions']}") - print("Unplaced Models:") - for model in self.unplaced_models: - print(f" - {model['name']} with dimensions {model['dimensions']}") - - def get_result(self): - return self.placed_models, self.unplaced_models - - def visualize(self): - """可视化排布结果""" - fig = plt.figure() - ax = fig.add_subplot(111, projection='3d') - ax.set_title("3D Printing Layout (Single Layer)") - ax.set_xlabel("X (Width)") - ax.set_ylabel("Y (Depth)") - ax.set_zlabel("Z (Height)") - - # 绘制平台边界 - ax.bar3d(0, 0, 0, self.width, self.depth, self.height, color='lightgray', alpha=0.1, edgecolor='black') - - # 绘制已放置的模型 - colors = ['b', 'g', 'r', 'c', 'm', 'y'] - for i, model in enumerate(self.placed_models): - x, y, z = model['position'] - dx, dy, dz = model['dimensions'] - color = colors[i % len(colors)] - ax.bar3d(x, y, z, dx, dy, dz, color=color, alpha=0.3, edgecolor='k') - ax.text(x + dx / 2, y + dy / 2, z + dz / 2, model['name'], color='black', fontsize=8, ha='center') - - ax.set_xlim(0, self.width) - ax.set_ylim(0, self.depth) - ax.set_zlim(0, self.height) - plt.show() - -def get_models_box_size(weight_fix_out_dir,show_chart,dict_fix,machine_size): - """获取排版的盒子大小""" - models = [] - # for ply_file in os.listdir(weight_fix_out_dir): - for ply_file in dict_fix: - # print("get_models_box_size", ply_file) - #print(ply_file.split("_")) - bbox_with_text = ply_file.split("=") - bbox_with = bbox_with_text[-1] - #print("盒子大小",bbox_with) - #time.sleep(1000) - split_text = bbox_with.replace(".ply","").split("+") - ply_pid = bbox_with_text[0] - extend_dist = 2 - x_length = int(float(split_text[2])*100) + extend_dist - y_length = int(float(split_text[0])*100) + extend_dist - z_length = int(float(split_text[1])*100) + extend_dist - #print("get_models_box_size",x_length,y_length,z_length) - models.append({'name':ply_file,'dimensions':(int(x_length/100),int(z_length/100),int(y_length/100))}) - #print(models) - # platform = Platform(int(38500 / 100), int(34000 / 100), int(25000 / 100)) - # platform = Platform(int(60000 / 100), int(50000 / 100), int(30000 / 100)) - platform = Platform(int(machine_size[0]), int(machine_size[1]), int(machine_size[2])) - print("开始计算排序...") - platform.arrange_models(models) - platform.print_results() - if show_chart: - platform.visualize() - return platform.get_result() - -def make_merged_pcd(): - # 创建 XY, XZ 和 YZ 平面的点云 - width = 1000 # 平面的宽度 - height = 1000 # 平面的高度 - resolution = 10 # 分辨率,控制点的密集程度 - - # XY 平面 (z=0) - x = np.linspace(-width / 2, width / 2, int(width / resolution)) # X 轴范围 - y = np.linspace(-height / 2, height / 2, int(height / resolution)) # Y 轴范围 - xv, yv = np.meshgrid(x, y) # 创建网格 - zv = np.zeros_like(xv) # z 坐标恒为 0 - points_xy = np.vstack((xv.flatten(), yv.flatten(), zv.flatten())).T - - # XZ 平面 (y=0) - z_xz = np.linspace(-height / 2, height / 2, int(height / resolution)) # Z 轴范围 - x_xz = np.linspace(-width / 2, width / 2, int(width / resolution)) # X 轴范围 - xv_xz, zv_xz = np.meshgrid(x_xz, z_xz) # 创建网格 - y_xz = np.zeros_like(xv_xz) # y 坐标恒为 0 - points_xz = np.vstack((xv_xz.flatten(), y_xz.flatten(), zv_xz.flatten())).T - - # YZ 平面 (x=0) - y_yz = np.linspace(-height / 2, height / 2, int(height / resolution)) # Y 轴范围 - z_yz = np.linspace(-width / 2, width / 2, int(width / resolution)) # Z 轴范围 - yv_yz, zv_yz = np.meshgrid(y_yz, z_yz) # 创建网格 - x_yz = np.zeros_like(yv_yz) # x 坐标恒为 0 - points_yz = np.vstack((x_yz.flatten(), yv_yz.flatten(), zv_yz.flatten())).T - - # 合并三个平面的点 - all_points = np.vstack((points_xy, points_xz, points_yz)) - - # 创建Open3D点云对象 - pcd_merged = o3d.geometry.PointCloud() - pcd_merged.points = o3d.utility.Vector3dVector(all_points) - pcd_merged.paint_uniform_color([1, 0, 0]) # 设置点云颜色为红色 - - return pcd_merged - -def read_mesh(obj_path, simple=True): - mesh_obj = o3d.io.read_triangle_mesh(obj_path) - return mesh_obj - if not simple: - return mesh_obj - original_triangles = len(mesh_obj.triangles) - target_triangles = original_triangles if original_triangles <= 10000 else 10000 - if original_triangles > 10000: - mesh_obj = mesh_obj.simplify_quadric_decimation( - target_number_of_triangles=target_triangles, - maximum_error=0.0001, - boundary_weight=1.0 - ) - - return mesh_obj - -def ply_print_layout_platform(weight_fix_out_obj_dir,weight_fix_out_dir,bounds_fix_out_dir,show_chart,dict_mesh_obj,dict_fix,dict_bounds_fix,machine_size,dict_total_matrix): - """根据排版结果移动点云到指定位置""" - placed_models,unplaced_models = get_models_box_size(weight_fix_out_dir,show_chart,dict_fix,machine_size) - if len(placed_models) ==0: - print("放进打印盒的数量为0") - return - # 创建坐标系 - draw_list = [] - if show_chart: - coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.2, origin=[0, 0, 0]) - pcd_plane = make_merged_pcd() - draw_list.append(coordinate_frame) - draw_list.append(pcd_plane) - print("ply_print_layout_platform placed_models") - for model in placed_models: - print(f" - {model['name']} at {model['position']} with dimensions {model['dimensions']}") - ply_file_name = model['name'] - move_position = model['position'] - ply_origin_path = os.path.join(weight_fix_out_dir,ply_file_name) - # print("要读取的点云数据路径",ply_origin_path) - # pcd = o3d.io.read_point_cloud(ply_origin_path) - pcd = dict_fix[ply_file_name] - # print("dict_fix read",ply_file_name,move_position) - - points = np.asarray(pcd.points) - min_bound = np.min(points, axis=0) # 获取点云的最小边界 - max_bound = np.max(points, axis=0) - min_bound[1] = max(min_bound[1], 0) - bbox_center = (min_bound + max_bound) / 2 # 计算包围盒的中心点 - bbox_extent = (max_bound - min_bound) - new_bbox = o3d.geometry.OrientedBoundingBox(center=bbox_center, - R=np.eye(3), # 旋转矩阵,默认没有旋转 - extent=bbox_extent) - x = move_position[0] - y = move_position[1] - z = move_position[2] - #move_position = np.array([x,y,z])/100 - move_position = np.array([x, y, z]) - # translation_vector = -move_position - translation_vector = move_position - pcd.translate(translation_vector) - new_bbox.translate(translation_vector) - obj_name = ply_file_name.split("=")[0]+".obj" - obj_path = os.path.join(weight_fix_out_obj_dir,obj_name) - - mesh_obj = dict_mesh_obj[obj_name] - # print("dict_mesh_obj",obj_name) - - mesh_obj.translate(translation_vector) - # o3d.io.write_triangle_mesh(obj_path, mesh_obj) - - T_trans1 = np.eye(4) - T_trans1[:3, 3] = translation_vector - dict_total_matrix[obj_name]= T_trans1 @ dict_total_matrix[obj_name] - - new_bbox_lines = o3d.geometry.LineSet.create_from_oriented_bounding_box(new_bbox) - new_bbox_lines.paint_uniform_color([1, 0, 0]) # 红色 - - ply_out_path = os.path.join(bounds_fix_out_dir, ply_file_name) - - """ - #试着旋转180,让脸朝上 - centroid = pcd.get_center() - z_mean1 = centroid[2] - - angle_deg = 180 - angle_rad = np.radians(angle_deg) # 转换为弧度 - - # 生成旋转矩阵(绕Z轴) - rotation_matrix = pcd.get_rotation_matrix_from_xyz((0, 0, angle_rad)) # 参数为(X,Y,Z轴的旋转弧度) - - # 执行旋转(绕点云中心旋转,避免位移) - center = pcd.get_center() # 获取点云质心坐标 - pcd.rotate(rotation_matrix, center=center) - - centroid = pcd.get_center() - z_mean2 = centroid[2] - - if z_mean2 > z_mean1: - rotation_matrix = pcd.get_rotation_matrix_from_xyz((-angle_rad, 0, 0)) # 参数为(X,Y,Z轴的旋转弧度) - center = pcd.get_center() # 获取点云质心坐标 - # pcd.rotate(rotation_matrix, center=center) - #""" - - # o3d.io.write_point_cloud(ply_out_path, pcd) - dict_bounds_fix[ply_file_name] = pcd - #o3d.visualization.draw_geometries([pcd,pcd_plane,coordinate_frame]) - if show_chart: - draw_list.append(pcd) - draw_list.append(new_bbox_lines) - if show_chart: - o3d.visualization.draw_geometries(draw_list) - - return placed_models - -def compute_distance2(pcd1, pcd2): - points1 = np.asarray(pcd1.points) - points2 = np.asarray(pcd2.points) - min_distance = float('inf') - - for p1 in points1: - distances = np.linalg.norm(points2 - p1, axis=1) - min_distance = min(min_distance, np.min(distances)) - - return min_distance - -def compute_distance(pcd1, pcd2): - """ - 正确计算两个点云之间距离的函数。 - 返回两个点云之间最近距离的平均值、最小值以及全部距离数组。 - """ - # 使用Open3D内置的高效方法计算距离 - # 计算pcd1中每个点到pcd2中最近点的距离 - distances = pcd1.compute_point_cloud_distance(pcd2) - distances = np.asarray(distances) - - # 计算有意义的统计量 - min_dist = np.min(distances) # 所有点中的最小距离 - mean_dist = np.mean(distances) # 距离的平均值 - - # return min_dist, mean_dist, distances - return min_dist - -def compute_distance_x(pcd1, pcd2): - points1 = np.asarray(pcd1.points)[:, 0] # 提取所有X坐标[3](@ref) - points2 = np.asarray(pcd2.points)[:, 0] - - x_diff = np.abs(points1[:, np.newaxis] - points2) - return np.min(x_diff) - -def compute_distance_y(pcd1, pcd2): - points1 = np.asarray(pcd1.points)[:, 1] # 提取所有Y坐标[3](@ref) - points2 = np.asarray(pcd2.points)[:, 1] - - y_diff = np.abs(points1.reshape(-1, 1) - points2) - return np.min(y_diff) - -def check_collision(pcd_moving, static_pcds,collision_threshold): - #print("检测碰撞中》》") - #print(f"collision_threshold{collision_threshold}") - - moving_points = np.asarray(pcd_moving.points) - min_distance_to_x_axis = np.min(np.abs(moving_points[:, 1])) # Y 坐标即为与 X 轴的距离 - #print(f"与 X 轴的最小距离: {min_distance_to_x_axis}") - min_distance_to_y_axis = np.min(np.abs(moving_points[:, 0])) # X 坐标即为与 Y 轴的距离 - #print(f"与 Y 轴的最小距离: {min_distance_to_y_axis}") - min_distance_to_z_axis = np.min(np.abs(moving_points[:, 2])) # X 坐标即为与 Y 轴的距离 - #print(f"与 Z 轴的最小距离: {min_distance_to_z_axis}") - #print(f"pcd_moving{len(static_pcds)}") - #if min_distance_to_x_axis < collision_threshold: - #print(f"与 X 轴发生碰撞! 最小距离: {min_distance_to_x_axis}") - # return True - #if min_distance_to_y_axis < collision_threshold: - #print(f"与 Y 轴发生碰撞! 最小距离: {min_distance_to_y_axis}") - # return True - if len(static_pcds)>0: - for static_pcd in static_pcds: - if static_pcd==pcd_moving: - continue - min_distance = compute_distance(pcd_moving, static_pcd) - #print(f"与点云的最小距离: {min_distance}") - if min_distance < collision_threshold: - #print(f"发生碰撞! 最小距离: {min_distance}") - return True - return False - -def check_collision_x(pcd_moving, static_pcds,collision_threshold): - moving_points = np.asarray(pcd_moving.points) - min_distance_to_x_axis = np.min(np.abs(moving_points[:, 1])) # Y 坐标即为与 X 轴的距离 - #print(f"与 X 轴的最小距离: {min_distance_to_x_axis}") - #print(f"与 Y 轴的最小距离: {min_distance_to_y_axis}") - #print(f"pcd_moving{len(static_pcds)}") - if min_distance_to_x_axis < collision_threshold: - print(f"与 X 轴发生碰撞! 最小距离: {min_distance_to_x_axis}") - return True - - return check_collision_all(pcd_moving, static_pcds,collision_threshold) - -def check_collision_y(pcd_moving, static_pcds,collision_threshold): - moving_points = np.asarray(pcd_moving.points) - #print(f"与 X 轴的最小距离: {min_distance_to_x_axis}") - min_distance_to_y_axis = np.min(np.abs(moving_points[:, 0])) # X 坐标即为与 Y 轴的距离 - #print(f"与 Y 轴的最小距离: {min_distance_to_y_axis}") - #print(f"pcd_moving{len(static_pcds)}") - if min_distance_to_y_axis < collision_threshold: - print(f"与 Y 轴发生碰撞! 最小距离: {min_distance_to_y_axis}") - return True - - return check_collision_all(pcd_moving, static_pcds,collision_threshold) - -def check_collision_all2(pcd_moving, static_pcds,collision_threshold): - if len(static_pcds)>0: - for static_pcd in static_pcds: - if static_pcd==pcd_moving: - continue - min_distance = compute_distance(pcd_moving, static_pcd) - #print(f"与点云的最小距离: {min_distance}") - if min_distance < collision_threshold: - #print(f"发生碰撞! 最小距离: {min_distance}") - return True - return False - -""" -import numpy as np -import numba -from numba import cuda -import math - -# 预加载静态点云到GPU显存 -static_gpu_arrays = [] - -def preload_static_pcds(static_pcds): - global static_gpu_arrays - static_gpu_arrays = [ - cuda.to_device(np.asarray(pcd.points)) - for pcd in static_pcds - ] - -@cuda.jit(device=True) -def point_distance(p1, p2): - dx = p1[0] - p2[0] - dy = p1[1] - p2[1] - dz = p1[2] - p2[2] - return math.sqrt(dx*dx + dy*dy + dz*dz) - -@cuda.jit -def collision_check_kernel(moving_points, static_points, threshold, collision_flag): - # 三维线程索引划分 - x, y, z = cuda.grid(3) - - # 获取当前处理的静态点云索引 - static_idx = z - - if static_idx >= len(static_points): - return - - # 共享内存缓存移动点云数据 - shared_moving = cuda.shared.array(shape=(32,3), dtype=numba.float32) - tx = cuda.threadIdx.x - if tx < moving_points.shape[0]: - shared_moving[tx, 0] = moving_points[tx, 0] - shared_moving[tx, 1] = moving_points[tx, 1] - shared_moving[tx, 2] = moving_points[tx, 2] - cuda.syncthreads() - - # 遍历当前静态点云的所有点 - static_pt = static_points[static_idx][y] - min_dist = math.inf - - # 并行计算移动点云各点与当前静态点的距离 - for i in range(moving_points.shape[0]): - dist = point_distance(shared_moving[i], static_pt) - if dist < min_dist: - min_dist = dist - - # 原子操作更新碰撞状态 - if min_dist < threshold: - cuda.atomic.min(collision_flag, 0, 1) - -def check_collision_all(pcd_moving, static_pcds, collision_threshold): - # 移动点云数据上传GPU - moving_points = np.asarray(pcd_moving.points) - d_moving = cuda.to_device(moving_points.astype(np.float32)) - - # 初始化碰撞标志 - d_collision = cuda.to_device(np.zeros(1, dtype=np.int32)) - - # 三维网格划分(静态点云数×单点云最大点数×移动点云数) - static_count = len(static_gpu_arrays) - max_static_points = max([arr.shape[0] for arr in static_gpu_arrays]) - - # 计算最优线程块配置 - threads_per_block = (8, 8, 1) - blocks_x = (moving_points.shape[0] + 7) // 8 - blocks_y = (max_static_points + 7) // 8 - blocks_z = static_count - - # 启动核函数 - collision_check_kernel[(blocks_x, blocks_y, blocks_z), threads_per_block]( - d_moving, - [arr for arr in static_gpu_arrays], # 静态点云列表 - np.float32(collision_threshold), - d_collision - ) - - # 获取结果 - collision_result = d_collision.copy_to_host() - return collision_result[0] == 1 -#""" -#""" -import numpy as np - -def compute_aabb(pcd): - """计算点云的AABB包围盒""" - points = np.asarray(pcd.points) - return { - 'min': np.min(points, axis=0), - 'max': np.max(points, axis=0) - } - -def aabb_intersect(a, b, collision_threshold): - """判断两个AABB包围盒是否相交[2,8](@ref)""" - return (a['max'][0] > b['min'][0] - collision_threshold and a['min'][0] < b['max'][0] + collision_threshold) and \ - (a['max'][1] > b['min'][1] - collision_threshold and a['min'][1] < b['max'][1] + collision_threshold) and \ - (a['max'][2] > b['min'][2] - collision_threshold and a['min'][2] < b['max'][2] + collision_threshold) - -def check_collision_all(pcd_moving, static_pcds, collision_threshold): - # 预计算移动点云AABB - moving_aabb = compute_aabb(pcd_moving) - - for static_pcd in static_pcds: - if static_pcd == pcd_moving: - continue - # 第一阶段:AABB快速排除[1,6](@ref) - static_aabb = compute_aabb(static_pcd) - # print("len(static_pcd.points)=",len(static_pcd.points),"len(moving_aabb.points)=",len(pcd_moving.points)) - if not aabb_intersect(moving_aabb, static_aabb, collision_threshold): - continue # 包围盒无交集,直接跳过 - - if not aabb_intersect(moving_aabb, static_aabb, collision_threshold): - return False - - # 第二阶段:精确点距离计算 - min_distance = compute_distance(pcd_moving, static_pcd) - # print("check_collision_all",min_distance) - if min_distance < collision_threshold: - return True - - return False - -#""" - -def compute_centroid(pcd): - # 获取点云的所有点 - points = np.asarray(pcd.points) - # 计算质心(只考虑 X 和 Y 坐标) - centroid = np.mean(points[:, :2], axis=0) # 只考虑前两个维度(X 和 Y) - return centroid - -def compute_distance_to_origin(centroid): - # 计算质心距离原点的距离(只考虑 X 和 Y 坐标) - return np.linalg.norm(centroid) # 计算 X 和 Y 的欧几里得距离 - -def compute_closest_distance_to_origin(pcd): - # 获取点云的所有点坐标 - points = np.asarray(pcd.points) - # 计算每个点到原点的距离 - distances = np.linalg.norm(points, axis=1) - # 返回最小距离 - return np.min(distances) - -def sort_ply_files_by_closest_distance(folder_path): - ply_files = [f for f in os.listdir(folder_path) if f.endswith('.ply')] - distances = [] - - for ply_file in ply_files: - # 读取点云数据 - pcd = o3d.io.read_point_cloud(os.path.join(folder_path, ply_file)) - # 计算离原点最近的点的距离 - closest_distance = compute_closest_distance_to_origin(pcd) - distances.append((ply_file, closest_distance)) - - # 按照最近点的距离排序(由近到远) - distances.sort(key=lambda x: x[1]) - - # 返回排序后的文件列表 - sorted_files = [item[0] for item in distances] - print("Sorted files:", sorted_files) - return sorted_files - -def ply_file_at_edge(folder_path): - ply_files = [f for f in os.listdir(folder_path) if f.endswith('.ply')] - edge_points = [] - - for ply_file in ply_files: - # 读取点云数据 - pcd = o3d.io.read_point_cloud(os.path.join(folder_path, ply_file)) - points = np.asarray(pcd.points) # Nx3 的点阵 - x = points[:, 0] - y = points[:, 1] - z = points[:, 2] - - # 计算每个点到 X 轴(Y=Z=0)的距离:√(y² + z²) - dist_to_x_axis = np.sqrt(y ** 2 + z ** 2) - - # 计算每个点到 Y 轴(X=Z=0)的距离:√(x² + z²) - dist_to_y_axis = np.sqrt(x ** 2 + z ** 2) - - # 示例:最小距离和对应的点 - min_x_dist_idx = np.argmin(dist_to_x_axis) - min_y_dist_idx = np.argmin(dist_to_y_axis) - print(f"点云中到 X 轴最近的距离: {dist_to_x_axis[min_x_dist_idx]:.4f}") - print(f"点云中到 Y 轴最近的距离: {dist_to_y_axis[min_y_dist_idx]:.4f}") - min_x_dis = dist_to_x_axis[min_x_dist_idx] - min_y_dix = dist_to_y_axis[min_y_dist_idx] - if min_x_dis<20: - edge_points.append(ply_file) - if min_y_dix<20: - edge_points.append(ply_file) - edge_points=list(set(edge_points)) - print(f"edge_points{edge_points}") - print(len(edge_points)) - - - return edge_points - -def compute_range(pcd): - points = np.asarray(pcd.points) # 获取点云中的点 - x_min, y_min = np.min(points[:, 0]), np.min(points[:, 1]) # X轴和Y轴的最小值 - x_max, y_max = np.max(points[:, 0]), np.max(points[:, 1]) # X轴和Y轴的最大值 - return (x_min, x_max), (y_min, y_max) - -def check_check_finish_which_touch(bounds_compact_out_dir,finish_pcd,finish_pid,exist_finish_pid_list,collision_threshold): - """""" - if len(exist_finish_pid_list)==0: - return [] - need_remove_list = [] - for exist_finish_pid in exist_finish_pid_list: - if exist_finish_pid!=finish_pid: - exist_finish_pcd_path= os.path.join(bounds_compact_out_dir, exist_finish_pid) - exist_finish_pcd= o3d.io.read_point_cloud(exist_finish_pcd_path) - min_distance = compute_distance(finish_pcd, exist_finish_pcd) - print(f"{exist_finish_pid}与点云的最小距离---阈值{collision_threshold}---: {min_distance}") - if min_distance < collision_threshold: - # print(f"发生碰撞! 最小距离: {min_distance}") - need_remove_list.append(exist_finish_pid) - print(f"需要删除的list::{need_remove_list}") - return need_remove_list - - -def filter_by_distance(points, original_center, threshold): - """ - 过滤掉距离 original_center 超过 threshold 的点。 - - :param points: N x 3 的 numpy 数组 - :param original_center: 1 x 3 的 numpy 数组或列表 - :param threshold: 距离阈值 - :return: 过滤后的点(仍是 N x 3 的 numpy 数组) - """ - points = np.array(points) - original_center = np.array(original_center) - - # 计算所有点到原始中心的欧几里得距离 - distances = np.linalg.norm(points - original_center, axis=1) - print(f"移动点和原始点的距离{distances}") - # 保留距离小于等于阈值的点 - filtered_points = points[distances <= threshold] - if len(filtered_points)==0: - return points - - return filtered_points - -def compact_mode_for_min_dis(input_dir, output_dir,show_chart,move_back,placed_models,dict_unplaced,dict_bounds_fix,dict_compact,machine_size): - # 小打印机380*345, 大打印机600*500*300 - y_step=1 - x_step=1 - delta = 10 - #""" - edge_x_min=-380 + delta - edge_y_min=-345 + delta - edge_x_max=0 - edge_y_max=0 - #""" - """ - edge_x_min=0 + delta - edge_y_min=0 + delta - edge_x_max=machine_size[0] - edge_y_max=machine_size[1] - #""" - collision_threshold=2 - move_last = True - # 清理输出目录 - if not os.path.exists(output_dir): - os.makedirs(output_dir) - for f in os.listdir(output_dir): - os.remove(os.path.join(output_dir, f)) - - pcd_all = [] - pcd_processed = [] - pcd_processed_x_top = [] - pcd_processed_no_x_top = [] - name_list = [] - model_list = [] - last_pcd_list = [] - last_name_list = [] - last_pcd_processed = [] - max_x = 0 - min_x = 9999 - max_delta_x = 0 - x_top_delta = 1 - border_delta = 4 - - pcd_first= [] - pcd_second= [] - for model in placed_models: - ply_origin_path = os.path.join(input_dir,model['name']) - # pcd = o3d.io.read_point_cloud(ply_origin_path) - pcd = dict_bounds_fix[model['name']] - pcd_all.append(pcd) - - if (get_axis_aligned_bbox(pcd)['y_min']>edge_y_max*0.3): - # if (True): - pcd_first.append(pcd) - print("add pcd_first", model['name']) - else: - pcd_second.append(pcd) - print("add pcd_second", model['name']) - last_name_list.append(model['name']) - - name_list.append(model['name']) - model_list.append(model) - dx = model['dimensions'][0] - x = model['position'][0] - - # if (x<=x_top_delta) : - if (x>=edge_x_max-x_top_delta) : - pcd_processed_x_top.append(pcd) - if dx > max_x: - max_x = dx - if dx < min_x: - min_x = dx - max_delta_x = max_x - min_x - - # print("compact_mode_for_min_dis", model, max_delta_x) - - draw_down = True - if max_delta_x < 10: - draw_down = False - #move_last = False - - # for idx, pcd in enumerate(pcd_all): - for idx, pcd in enumerate(pcd_first): - x = model_list[idx]['position'][0] - y = model_list[idx]['position'][1] - dx = model_list[idx]['dimensions'][0] - # print("compact_mode", name_list[idx], dx, x) - dist_x = 50 - dist_y = 20 - is_x_top = False - if x - 10 < edge_x_min: - dist_x = x - edge_x_min - if y - 10 < edge_y_min: - dist_y = y - edge_y_min - # if (x>x_top_delta) : - if (x 80: - y_init_big = 10 - x_init_big = y_init_big - 1 - """ - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_max'] >= edge_y_max - collision_threshold: - pcd.translate([-x_step_big, -y_step_big, 0]) - print("compact_mode y_max", idx, bbox['y_max'], edge_y_max - collision_threshold_big) - break - if bbox['x_max'] >= edge_x_max - collision_threshold: - pcd.translate([-x_step_big, -y_step_big, 0]) - print("compact_mode x_max", idx, bbox['x_max'], edge_x_max - collision_threshold_big) - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big): - pcd.translate([-x_step_big, -y_step_big, 0]) - break - pcd.translate([x_step_big, y_step_big, 0]) - #""" - #""" - while True: - bbox = get_axis_aligned_bbox(pcd) - # print("x_max",bbox['x_max'],bbox['x_min'],bbox['y_max'],bbox['y_min']) - if bbox['y_min'] <= edge_y_min + collision_threshold_big and False: - pcd.translate([0, y_step_big, 0]) - break - if bbox['y_max'] >= edge_y_max - collision_threshold_big: - pcd.translate([0, -y_step_big, 0]) - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big+y_init_big): #5 - pcd.translate([0, -y_step_big, 0]) - break - pcd.translate([0, y_step_big, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + collision_threshold_big and False: - pcd.translate([x_step_big, 0, 0]) - break - if bbox['x_max'] >= edge_x_max - collision_threshold_big: - pcd.translate([-x_step_big, 0, 0]) - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big+x_init_big): - pcd.translate([-x_step_big, 0, 0]) - break - pcd.translate([x_step_big, 0, 0]) - #""" - #""" - collision_threshold_init = collision_threshold+6 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: - pcd.translate([0, y_step, 0]) - break - # if bbox['y_max'] >= edge_y_max - collision_threshold_init: - if bbox['y_max'] >= edge_y_max - border_delta: - pcd.translate([0, -y_step, 0]) - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init+1): #5 - pcd.translate([0, -y_step, 0]) - break - pcd.translate([0, y_step, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + collision_threshold_init and False: - pcd.translate([x_step, 0, 0]) - break - # if bbox['x_max'] >= edge_x_max - collision_threshold_init: - if bbox['x_max'] >= edge_x_max - border_delta: - pcd.translate([-x_step, 0, 0]) - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): - pcd.translate([-x_step, 0, 0]) - break - pcd.translate([x_step, 0, 0]) - #""" - #""" - collision_threshold_init = collision_threshold+2 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: - pcd.translate([0, y_step, 0]) - break - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step, 0]) - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init+1): #5 - pcd.translate([0, -y_step, 0]) - break - pcd.translate([0, y_step, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + collision_threshold_init and False: - pcd.translate([x_step, 0, 0]) - break - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - pcd.translate([-x_step, 0, 0]) - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): - pcd.translate([-x_step, 0, 0]) - break - pcd.translate([x_step, 0, 0]) - # place again - collision_threshold_init = collision_threshold+1 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: - pcd.translate([0, y_step, 0]) - break - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step, 0]) - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): #5 - pcd.translate([0, -y_step, 0]) - break - pcd.translate([0, y_step, 0]) - #""" - pcd_processed.append(pcd) - pcd_processed_x_top.append(pcd) - if not is_x_top: - pcd_processed_no_x_top.append(pcd) - - cross_border = False - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + 1 or bbox['y_min'] <= edge_y_min + 1: - cross_border = True - print("coross_border",name_list[idx]) - - print(name_list[idx],"dx",dx,"y_min",get_axis_aligned_bbox(pcd)['y_min'],cross_border) - # if (dx <= 120 and get_axis_aligned_bbox(pcd)['y_min'] < -250 and move_last) or cross_border: - # last_name_list.append(name_list[idx]) - # last_pcd_list.append(pcd) - # print("last_pcd_list",name_list[idx],"dx",dx,"y_min",get_axis_aligned_bbox(pcd)['y_min']) - # else: - # # o3d.io.write_point_cloud(os.path.join(output_dir, name_list[idx]), pcd) - # dict_compact[name_list[idx]] = pcd - # last_pcd_processed.append(pcd) - - if cross_border: - pcd_second.append(pcd) - last_name_list.append(name_list[idx]) - print("cross_border", name_list[idx]) - else: - print("Add dict_compact", name_list[idx]) - dict_compact[name_list[idx]] = pcd - last_pcd_processed.append(pcd) - - volumes = [] - # for idx, pcd in enumerate(last_pcd_list): - for idx, pcd in enumerate(pcd_second): - bbox = get_axis_aligned_bbox(pcd) - - x_length = bbox['x_max'] - bbox['x_min'] - y_length = bbox['y_max'] - bbox['y_min'] - z_length = bbox['z_max'] - bbox['z_min'] - volume = x_length * y_length * z_length - volumes.append(volume) - - print("last_pcd_list", len(last_pcd_list), len(last_pcd_list), len(last_pcd_processed), len(pcd_all)) - sorted_indices = np.argsort(volumes)[::-1] - pcd_second2 = [pcd_second[i] for i in sorted_indices] - last_name_list2 = [last_name_list[i] for i in sorted_indices] - # print("last_pcd_list2", len(last_pcd_list2)) - - #last_pcd_list2 = last_pcd_list - #last_name_list2 = last_name_list - - for idx, pcd in enumerate(pcd_second2): - points = np.asarray(pcd.points) - min_x = np.min(points[:, 0]) - max_y = np.max(points[:, 1]) # 当前最大y值 - - tx = edge_x_min - min_x - ty = -max_y - 0.001 - - # pcd.translate((tx, ty, 0), relative=True) - - move_to_top_left(pcd, edge_x_min+2, edge_y_max-2) - - name = last_name_list2[idx] - # print("pcd_second2",name,"tx",tx,"ty",ty) - succ_move = True - y_accum = 0 - finish_move2 = False - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_min'] <= edge_y_min + collision_threshold_init: - print("succ_move False",name,bbox['y_min'],edge_y_min + collision_threshold_init) - succ_move = False - finish_move2 = True - """ - move_to_top_left(pcd, edge_x_min+2, edge_y_max-2) - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init2 = collision_threshold - if bbox['y_min'] <= edge_y_min + collision_threshold_init2: - print("succ_move False2",name,bbox['y_min'],edge_y_min + collision_threshold_init2) - succ_move = False - finish_move2 = True - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init2): - print("succ_move 2") - finish_move2 = True - break - pcd.translate([0, -y_step, 0]) - """ - - if (finish_move2): - break - else: - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - print("succ_move1",name,bbox['x_max'],bbox['y_max'],len(last_pcd_processed)) - break - - pcd.translate([0, -y_step, 0]) - y_accum += y_step - if succ_move: - print("succ_move2", name) - """ - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_max'] >= edge_x_max - collision_threshold_big: - pcd.translate([-x_step_big, 0, 0]) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_big): - pcd.translate([-x_step_big, 0, 0]) - break - pcd.translate([x_step_big, 0, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_max'] >= edge_y_max - collision_threshold: - pcd.translate([0, -y_step, 0]) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold+1): #5 - pcd.translate([0, -y_step, 0]) - break - pcd.translate([0, y_step, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_max'] >= edge_x_max - collision_threshold: - pcd.translate([-x_step, 0, 0]) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold): - pcd.translate([-x_step, 0, 0]) - break - pcd.translate([x_step, 0, 0]) - #""" - - #""" - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+2 - #print("Move x_step_big", name, bbox['y_max'], bbox['x_max']) - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - pcd.translate([-x_accu, 0, 0]) - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - #print("Move y_max", name, bbox['y_max'], bbox['x_max']) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - pcd.translate([0, -y_step_big, 0]) - #print("Move y_max2", name, bbox['y_max'], bbox['x_max']) - break - pcd.translate([0, y_step_big, 0]) - #print("Move y_step_big", name, bbox['y_max'], bbox['x_max']) - else: - n = 1 - - x_accu += x_step_big - pcd.translate([x_step_big, 0, 0]) - #""" - else: - # pcd.translate((-tx, -ty+y_accum, 0), relative=True) - points = np.asarray(pcd.points) - min_x = np.min(points[:, 0]) - min_y = np.min(points[:, 1]) - tx = edge_x_min - min_x - ty = edge_y_min - min_y - pcd.translate((tx, ty, 0), relative=True) - - print("last place", name) - - #""" - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+10 - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - print("fail to place",name) - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - print("last place2",name) - break - pcd.translate([x_step_big, 0, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+3 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - print("last place3",name) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): #5 - pcd.translate([0, -y_step_big, 0]) - print("last place4",name,collision_threshold_init,len(last_pcd_processed)) - break - pcd.translate([0, y_step_big, 0]) - print("last place41",y_step_big) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_max'] >= edge_y_max - collision_threshold: - pcd.translate([0, -y_step, 0]) - print("last place5",name) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold+1): #5 - pcd.translate([0, -y_step, 0]) - print("last place6",name) - break - pcd.translate([0, y_step, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_max'] >= edge_x_max - collision_threshold: - pcd.translate([-x_step, 0, 0]) - print("last place7",name) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold): - pcd.translate([-x_step, 0, 0]) - print("last place8",name) - break - pcd.translate([x_step, 0, 0]) - #""" - - """ - can_place_last = False - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+2 - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - if not can_place_last: - print("fail to place",name) - dict_unplaced[name]=name - else: - pcd.translate([-x_accu, 0, 0]) - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - can_place_last = True - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - pcd.translate([0, -y_step_big, 0]) - break - pcd.translate([0, y_step_big, 0]) - #print("Move2 y_step_big", name) - else: - n = 1 - - x_accu += x_step_big - pcd.translate([x_step_big, 0, 0]) - #""" - #""" - can_place_last = False - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+2 - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - if not can_place_last: - print("fail to place",name) - dict_unplaced[name]=name - else: - pcd.translate([-x_accu, 0, 0]) - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - can_place_last = True - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - pcd.translate([0, -y_step_big, 0]) - break - pcd.translate([0, y_step_big, 0]) - #print("Move2 y_step_big", name) - else: - n = 1 - - x_accu += x_step_big - pcd.translate([x_step_big, 0, 0]) - #""" - - last_pcd_processed.append(pcd) - """ - print("is_x_top",is_x_top) - if not is_x_top: - last_pcd_processed.append(pcd) - print("last_pcd_processed.append",name) - else: - print("fail last_pcd_processed.append",name, is_x_top) - """ - # o3d.io.write_point_cloud(os.path.join(output_dir, name), pcd) - dict_compact[name] = pcd - - print("2Add dict_compact", name) - -def compact_mode_for_min_dis1_json(input_dir,output_dir,show_chart,move_back,placed_models,dict_unplaced,dict_bounds_fix,dict_compact,machine_size,dict_total_matrix): - # 小打印机380*345*250, 大打印机600*500*300 - y_step=1 - x_step=1 - delta = 10 - - edge_x_min=0 + delta - edge_y_min=0 + delta - edge_x_max=machine_size[0] - edge_y_max=machine_size[1] - - collision_threshold=2 - move_last = True - - # 清理输出目录 - if not os.path.exists(output_dir): - os.makedirs(output_dir) - for f in os.listdir(output_dir): - os.remove(os.path.join(output_dir, f)) - - pcd_all = [] - pcd_processed = [] - pcd_processed_x_top = [] - pcd_processed_no_x_top = [] - # name_list = [] - dict_name = {} - # model_list = [] - dict_model = {} - last_pcd_list = [] - # last_name_list = [] - dic_last_name = {} - last_pcd_processed = [] - max_x = machine_size[0] - min_x = 0 - max_delta_x = 0 - x_top_delta = 1 - border_delta = 4 - - pcd_first= [] - pcd_second= [] - - index = 0 - for model in placed_models: - pcd = dict_bounds_fix[model['name']] - - num_samples = min(1500, len(pcd.points)) - - pcd_all.append(pcd) - - if (get_axis_aligned_bbox(pcd)['y_min']>edge_y_max*0.3 or True): - pcd_first.append(pcd) - else: - pcd_second.append(pcd) - - # pcd_all.append(pcd_downsampled) - # name_list.append(model['name']) - # model_list.append(model) - dict_name[pcd] = model['name'] - dict_model[pcd] = model - dx = model['dimensions'][0] - x = model['position'][0] - - if (x>=edge_x_max-x_top_delta) : - pcd_processed_x_top.append(pcd) - print("pcd_processed_x_top", model['name']) - if dx > max_x: - max_x = dx - if dx < min_x: - min_x = dx - max_delta_x = max_x - min_x - - index += 1 - - # print("compact_mode_for_min_dis1_json", model, max_delta_x) - - draw_down = False - if max_delta_x < 10: - draw_down = False - - # for idx, pcd in enumerate(pcd_all): - for idx, pcd in enumerate(pcd_first): - - if dict_model[pcd]['first_line']: - pcd_processed.append(pcd) - last_pcd_processed.append(pcd) - continue - - x = dict_model[pcd]['position'][0] - y = dict_model[pcd]['position'][1] - dx = dict_model[pcd]['dimensions'][0] - print("compact_mode", dict_name[pcd], dx, x) - - ply_file_name = dict_name[pcd] - obj_name = ply_file_name.split("=")[0]+".obj" - - T_trans1 = np.eye(4) - - dist_x = 50 - dist_y = 20 - is_x_top = False - if x - 10 < edge_x_min: - dist_x = x - edge_x_min - if y - 10 < edge_y_min: - dist_y = y - edge_y_min - if (x 80: - y_init_big = 10 - x_init_big = y_init_big - 1 - - if check_collision_all(pcd, pcd_processed_curr, 1): - while True: - step = 25 - pcd.translate([0, -step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -step, 0] - T_trans1 = T_transTemp @ T_trans1 - # pcd.translate([-step, 0, 0]) - # T_transTemp[:3, 3] = [-step, 0, 0] - # T_trans1 = T_transTemp @ T_trans1 - - if not check_collision_all(pcd, pcd_processed_curr, collision_threshold_big): - break - - """ - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_max'] >= edge_y_max - collision_threshold: - pcd.translate([-x_step_big, -y_step_big, 0]) - print("compact_mode y_max", idx, bbox['y_max'], edge_y_max - collision_threshold_big) - break - if bbox['x_max'] >= edge_x_max - collision_threshold: - pcd.translate([-x_step_big, -y_step_big, 0]) - print("compact_mode x_max", idx, bbox['x_max'], edge_x_max - collision_threshold_big) - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big): - pcd.translate([-x_step_big, -y_step_big, 0]) - break - pcd.translate([x_step_big, y_step_big, 0]) - #""" - #""" - while True: - bbox = get_axis_aligned_bbox(pcd) - # print("x_max",bbox['x_max'],bbox['x_min'],bbox['y_max'],bbox['y_min']) - if bbox['y_min'] <= edge_y_min + collision_threshold_big and False: - pcd.translate([0, y_step_big, 0]) - break - if bbox['y_max'] >= edge_y_max - collision_threshold_big: - pcd.translate([0, -y_step_big, 0]) - - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big+y_init_big): #5 - pcd.translate([0, -y_step_big, 0]) - - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - - break - pcd.translate([0, y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + collision_threshold_big and False: - pcd.translate([x_step_big, 0, 0]) - break - if bbox['x_max'] >= edge_x_max - collision_threshold_big: - pcd.translate([-x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big+x_init_big): - pcd.translate([-x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - #""" - collision_threshold_init = collision_threshold+6 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: - pcd.translate([0, y_step, 0]) - break - # if bbox['y_max'] >= edge_y_max - collision_threshold_init: - if bbox['y_max'] >= edge_y_max - border_delta: - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init+1): #5 - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([0, y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - #""" - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + collision_threshold_init and False: - pcd.translate([x_step, 0, 0]) - break - # if bbox['x_max'] >= edge_x_max - collision_threshold_init: - if bbox['x_max'] >= edge_x_max - border_delta: - # print("1pcd.translate([-x_step, 0, 0])",name_list[idx]) - pcd.translate([-x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): - # print("2pcd.translate([-x_step, 0, 0])",name_list[idx]) - pcd.translate([-x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - #""" - collision_threshold_init = collision_threshold+2 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: - pcd.translate([0, y_step, 0]) - break - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init+1): #5 - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([0, y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + collision_threshold_init and False: - pcd.translate([x_step, 0, 0]) - break - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - pcd.translate([-x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): - pcd.translate([-x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - # place again - collision_threshold_init = collision_threshold+1 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: - pcd.translate([0, y_step, 0]) - break - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): #5 - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([0, y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - pcd_processed.append(pcd) - pcd_processed_x_top.append(pcd) - if not is_x_top: - pcd_processed_no_x_top.append(pcd) - - cross_border = False - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + 1 or bbox['y_min'] <= edge_y_min + 1: - cross_border = True - print("coross_border",ply_file_name) - - # print(ply_file_name,"dx",dx,"y_min",get_axis_aligned_bbox(pcd)['y_min'],cross_border) - # if (get_axis_aligned_bbox(pcd)['y_min']= edge_x_max - collision_threshold_big: - pcd.translate([-x_step_big, 0, 0]) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_big): - pcd.translate([-x_step_big, 0, 0]) - break - pcd.translate([x_step_big, 0, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_max'] >= edge_y_max - collision_threshold: - pcd.translate([0, -y_step, 0]) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold+1): #5 - pcd.translate([0, -y_step, 0]) - break - pcd.translate([0, y_step, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_max'] >= edge_x_max - collision_threshold: - pcd.translate([-x_step, 0, 0]) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold): - pcd.translate([-x_step, 0, 0]) - break - pcd.translate([x_step, 0, 0]) - #""" - - #""" - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+2 - #print("Move x_step_big", name, bbox['y_max'], bbox['x_max']) - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - pcd.translate([-x_accu, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_accu, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - #print("Move y_max", name, bbox['y_max'], bbox['x_max']) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - #print("Move y_max2", name, bbox['y_max'], bbox['x_max']) - break - pcd.translate([0, y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - #print("Move y_step_big", name, bbox['y_max'], bbox['x_max']) - else: - n = 1 - - x_accu += x_step_big - pcd.translate([x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - else: - # T_transTemp = move_to_bottom_left(pcd, edge_x_min, edge_y_min) - T_transTemp = move_to_bottom_right(pcd, edge_x_max, edge_y_min) - T_trans1 = T_transTemp @ T_trans1 - - print("last place", name) - - """ - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+10 - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - print("fail to place",name) - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - print("last place2",name) - break - pcd.translate([x_step_big, 0, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+3 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - print("last place3",name) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): #5 - pcd.translate([0, -y_step_big, 0]) - print("last place4",name,collision_threshold_init,len(last_pcd_processed)) - break - pcd.translate([0, y_step_big, 0]) - print("last place41",y_step_big) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_max'] >= edge_y_max - collision_threshold: - pcd.translate([0, -y_step, 0]) - print("last place5",name) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold+1): #5 - pcd.translate([0, -y_step, 0]) - print("last place6",name) - break - pcd.translate([0, y_step, 0]) - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_max'] >= edge_x_max - collision_threshold: - pcd.translate([-x_step, 0, 0]) - print("last place7",name) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold): - pcd.translate([-x_step, 0, 0]) - print("last place8",name) - break - pcd.translate([x_step, 0, 0]) - #""" - - """ - can_place_last = False - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+2 - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - if not can_place_last: - print("fail to place",name) - dict_unplaced[name]=name - else: - pcd.translate([-x_accu, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_accu, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - can_place_last = True - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([0, y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - #print("Move2 y_step_big", name) - else: - n = 1 - - x_accu += x_step_big - pcd.translate([x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - - #""" - can_place_last = False - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+2 - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - if not can_place_last: - print("fail to place",name) - dict_unplaced[name]=name - else: - pcd.translate([-x_accu, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_accu, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - can_place_last = True - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([0, y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - #print("Move2 y_step_big", name) - else: - n = 1 - - x_accu += x_step_big - pcd.translate([x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - - """ - can_place_last = False - x_accu = 0 - place_first = True - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+2 - if bbox['x_min'] <= edge_x_min + collision_threshold_init: - if not can_place_last: - print("fail to place",name) - dict_unplaced[name]=name - else: - pcd.translate([+x_accu, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [+x_accu, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - place_first = True - print("place_first True") - break - can_break = False - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - can_place_last = True - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - can_break = True - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - if place_first: - can_break = True - break - pcd.translate([0, y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - #print("Move2 y_step_big", name) - else: - n = 1 - - if can_break: - break - - x_accu += x_step_big - pcd.translate([-x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - - last_pcd_processed.append(pcd) - """ - print("is_x_top",is_x_top) - if not is_x_top: - last_pcd_processed.append(pcd) - print("last_pcd_processed.append",name) - else: - print("fail last_pcd_processed.append",name, is_x_top) - """ - # o3d.io.write_point_cloud(os.path.join(output_dir, name), pcd) - dict_compact[name] = pcd - - dict_total_matrix[obj_name]= T_trans1 @ dict_total_matrix[obj_name] - -def compact_mode_for_min_dis2_json(input_dir,output_dir,show_chart,move_back,placed_models,dict_unplaced,dict_bounds_fix,dict_compact,machine_size,dict_total_matrix): - # 小打印机380*345, 大打印机600*500*300 - y_step=1 - x_step=1 - delta = 10 - - edge_x_min=0 + delta - edge_y_min=0 + delta - edge_x_max=machine_size[0] - edge_y_max=machine_size[1] - - collision_threshold=2 - # move_last = True - move_last = False - - # 清理输出目录 - if not os.path.exists(output_dir): - os.makedirs(output_dir) - for f in os.listdir(output_dir): - os.remove(os.path.join(output_dir, f)) - - pcd_all = [] - pcd_processed = [] - pcd_processed_x_top = [] - pcd_processed_no_x_top = [] - # name_list = [] - dict_name = {} - # model_list = [] - dict_model = {} - last_pcd_list = [] - # last_name_list = [] - dic_last_name = {} - last_pcd_processed = [] - max_x = machine_size[0] - min_x = 0 - max_delta_x = 0 - x_top_delta = 1 - border_delta = 4 - for model in placed_models: - pcd = dict_bounds_fix[model['name']] - - pcd_all.append(pcd) - - dict_name[pcd] = model['name'] - dict_model[pcd] = model - dx = model['dimensions'][0] - x = model['position'][0] - - if (x>=edge_x_max-x_top_delta) : - pcd_processed_x_top.append(pcd) - print("pcd_processed_x_top", model['name']) - if dx > max_x: - max_x = dx - if dx < min_x: - min_x = dx - max_delta_x = max_x - min_x - - # print("compact_mode_for_min_dis2_json", model, max_delta_x) - - # draw_down = True - draw_down = False - if max_delta_x < 10: - draw_down = False - - for idx, pcd in enumerate(pcd_all): - x = dict_model[pcd]['position'][0] - y = dict_model[pcd]['position'][1] - dx = dict_model[pcd]['dimensions'][0] - # print("compact_mode", dict_name[pcd], dx, x) - - ply_file_name = dict_name[pcd] - obj_name = ply_file_name.split("=")[0]+".obj" - - T_trans1 = np.eye(4) - - dist_x = 50 - dist_y = 20 - is_x_top = False - if x - 10 < edge_x_min: - dist_x = x - edge_x_min - if y - 10 < edge_y_min: - dist_y = y - edge_y_min - if (x 80: - y_init_big = 10 - x_init_big = y_init_big - 1 - #""" - while True: - bbox = get_axis_aligned_bbox(pcd) - # print("x_max",bbox['x_max'],bbox['x_min'],bbox['y_max'],bbox['y_min']) - if bbox['y_min'] <= edge_y_min + collision_threshold_big and False: - pcd.translate([0, y_step_big, 0]) - break - if bbox['y_max'] >= edge_y_max - collision_threshold_big: - pcd.translate([0, -y_step_big, 0]) - - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big+y_init_big): #5 - pcd.translate([0, -y_step_big, 0]) - - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - - break - pcd.translate([0, y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + collision_threshold_big and False: - pcd.translate([x_step_big, 0, 0]) - break - if bbox['x_max'] >= edge_x_max - collision_threshold_big: - pcd.translate([-x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_big+x_init_big): - pcd.translate([-x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - #""" - collision_threshold_init = collision_threshold+6 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: - pcd.translate([0, y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - # if bbox['y_max'] >= edge_y_max - collision_threshold_init: - if bbox['y_max'] >= edge_y_max - border_delta: - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init+1): #5 - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([0, y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - #""" - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + collision_threshold_init and False: - pcd.translate([x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - # if bbox['x_max'] >= edge_x_max - collision_threshold_init: - if bbox['x_max'] >= edge_x_max - border_delta: - # print("1pcd.translate([-x_step, 0, 0])",name_list[idx]) - pcd.translate([-x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): - # print("2pcd.translate([-x_step, 0, 0])",name_list[idx]) - pcd.translate([-x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - #""" - collision_threshold_init = collision_threshold+2 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: - pcd.translate([0, y_step, 0]) - break - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init+1): #5 - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([0, y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + collision_threshold_init and False: - pcd.translate([x_step, 0, 0]) - break - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - pcd.translate([-x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): - pcd.translate([-x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([x_step, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - # place again - collision_threshold_init = collision_threshold+1 - while True: - bbox = get_axis_aligned_bbox(pcd) - if bbox['y_min'] <= edge_y_min + collision_threshold_init and False: - pcd.translate([0, y_step, 0]) - break - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, pcd_processed_curr,collision_threshold_init): #5 - pcd.translate([0, -y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([0, y_step, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - pcd_processed.append(pcd) - pcd_processed_x_top.append(pcd) - if not is_x_top: - pcd_processed_no_x_top.append(pcd) - - cross_border = False - bbox = get_axis_aligned_bbox(pcd) - if bbox['x_min'] <= edge_x_min + 1 or bbox['y_min'] <= edge_y_min + 1: - cross_border = True - print("coross_border",ply_file_name) - - print(ply_file_name,"dx",dx,"y_min",get_axis_aligned_bbox(pcd)['y_min'],cross_border) - if (dx <= 120 and get_axis_aligned_bbox(pcd)['y_min'] < 250 and move_last) or cross_border: - last_pcd_list.append(pcd) - dic_last_name[pcd] = ply_file_name - print("last_pcd_list",ply_file_name,"dx",dx,"y_min",get_axis_aligned_bbox(pcd)['y_min']) - else: - # o3d.io.write_point_cloud(os.path.join(output_dir, name_list[idx]), pcd) - dict_compact[ply_file_name] = pcd - last_pcd_processed.append(pcd) - - dict_total_matrix[obj_name]= T_trans1 @ dict_total_matrix[obj_name] - - volumes = [] - for idx, pcd in enumerate(last_pcd_list): - bbox = get_axis_aligned_bbox(pcd) - - x_length = bbox['x_max'] - bbox['x_min'] - y_length = bbox['y_max'] - bbox['y_min'] - z_length = bbox['z_max'] - bbox['z_min'] - volume = x_length * y_length * z_length - volumes.append(volume) - - print(f"last_pcd_list len(last_pcd_list)={len(last_pcd_list)},len(last_pcd_processed)={len(last_pcd_processed)},len(pcd_all)={len(pcd_all)}") - sorted_indices = np.argsort(volumes)[::-1] - last_pcd_list2 = [last_pcd_list[i] for i in sorted_indices] - # last_name_list2 = [last_name_list[i] for i in sorted_indices] - print("last_pcd_list2", len(last_pcd_list2)) - - for idx, pcd in enumerate(last_pcd_list2): - - ply_file_name = dict_name[pcd] - obj_name = ply_file_name.split("=")[0]+".obj" - - print("last_pcd_list2", obj_name) - - T_trans1 = np.eye(4) - - points = np.asarray(pcd.points) - min_x = np.min(points[:, 0]) - max_y = np.max(points[:, 1]) # 当前最大y值 - - tx = edge_x_min - min_x - ty = -max_y - 0.001 - - T_transTemp = move_to_top_left(pcd, edge_x_min+2, edge_y_max-2) - - T_trans1 = T_transTemp @ T_trans1 - - name = dict_name[pcd] - # print("last_pcd_list2",name,"tx",tx,"ty",ty) - succ_move = True - y_accum = 0 - finish_move2 = False - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_min'] <= edge_y_min + collision_threshold_init: - print("succ_move False",name,bbox['y_min'],edge_y_min + collision_threshold_init) - succ_move = False - finish_move2 = True - - if (finish_move2): - break - else: - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - print("succ_move1",name,bbox['x_max'],bbox['y_max'],len(last_pcd_processed)) - break - - pcd.translate([0, -y_step, 0]) - - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step, 0] - T_trans1 = T_transTemp @ T_trans1 - - y_accum += y_step - - if succ_move: - #print("succ_move2", name) - #""" - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+2 - #print("Move x_step_big", name, bbox['y_max'], bbox['x_max']) - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - pcd.translate([-x_accu, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_accu, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - #print("Move y_max", name, bbox['y_max'], bbox['x_max']) - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - #print("Move y_max2", name, bbox['y_max'], bbox['x_max']) - break - pcd.translate([0, y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - #print("Move y_step_big", name, bbox['y_max'], bbox['x_max']) - else: - n = 1 - - x_accu += x_step_big - pcd.translate([x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - else: - points = np.asarray(pcd.points) - min_x = np.min(points[:, 0]) - min_y = np.min(points[:, 1]) - tx = edge_x_min - min_x - ty = edge_y_min - min_y - pcd.translate((tx, ty, 0), relative=True) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [tx, ty, 0] - T_trans1 = T_transTemp @ T_trans1 - - print("last place", name) - - #""" - can_place_last = False - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+2 - if bbox['x_max'] >= edge_x_max - collision_threshold_init: - if not can_place_last: - print("fail to place",name) - dict_unplaced[name]=name - else: - pcd.translate([-x_accu, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [-x_accu, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if not check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - can_place_last = True - x_accu = 0 - while True: - bbox = get_axis_aligned_bbox(pcd) - collision_threshold_init = collision_threshold+1 - if bbox['y_max'] >= edge_y_max - collision_threshold_init: - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - break - if check_collision_all(pcd, last_pcd_processed,collision_threshold_init): - pcd.translate([0, -y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, -y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - break - pcd.translate([0, y_step_big, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [0, y_step_big, 0] - T_trans1 = T_transTemp @ T_trans1 - else: - n = 1 - - x_accu += x_step_big - pcd.translate([x_step_big, 0, 0]) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [x_step_big, 0, 0] - T_trans1 = T_transTemp @ T_trans1 - #""" - - last_pcd_processed.append(pcd) - - dict_compact[name] = pcd - - dict_total_matrix[obj_name]= T_trans1 @ dict_total_matrix[obj_name] - -def move_to_top_left(pcd, edge_x_min, edge_y_max): - points = np.asarray(pcd.points) - min_x = np.min(points[:, 0]) - max_y = np.max(points[:, 1]) # 当前最大y值 - - tx = edge_x_min - min_x - # ty = -max_y - 0.001 - ty = edge_y_max - max_y - - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [tx, ty, 0] - - pcd.translate((tx, ty, 0), relative=True) - - return T_transTemp - -def move_to_bottom_left(pcd, edge_x_min, edge_y_min): - - points = np.asarray(pcd.points) - min_x = np.min(points[:, 0]) - min_y = np.min(points[:, 1]) - tx = edge_x_min - min_x - ty = edge_y_min - min_y - pcd.translate((tx, ty, 0), relative=True) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [tx, ty, 0] - - return T_transTemp - -def move_to_bottom_right(pcd, edge_x_max, edge_y_min): - - points = np.asarray(pcd.points) - max_x = np.min(points[:, 0]) - min_y = np.min(points[:, 1]) - tx = edge_x_max - max_x - ty = edge_y_min - min_y - pcd.translate((tx, ty, 0), relative=True) - T_transTemp = np.eye(4) - T_transTemp[:3, 3] = [tx, ty, 0] - - return T_transTemp - -def get_y_centroid(file_path): - pcd = o3d.io.read_point_cloud(file_path) - return np.mean(np.asarray(pcd.points)[:,1]) - -def get_axis_aligned_bbox(pcd): - points = np.asarray(pcd.points) - return { - 'x_min': np.min(points[:,0]), - 'x_max': np.max(points[:,0]), - 'y_min': np.min(points[:,1]), - 'y_max': np.max(points[:,1]), - 'z_min': np.min(points[:,2]), - 'z_max': np.max(points[:,2]) - } - -def check_x_collision(target_bbox, placed_models,collision_threshold=2): - for model in placed_models: - print(target_bbox['x_max'],target_bbox['x_min'],model['bbox']['x_max'],model['bbox']['x_min']) - if (target_bbox['x_max']+collision_threshold > model['bbox']['x_min']): - return True - return False - -def check_y_collision(target_bbox, placed_models,collision_threshold=2): - for model in placed_models: - if (target_bbox['y_max']+collision_threshold > model['bbox']['y_max']): - return True - return False - -def down_sample(pcd, voxel_size, farthest_sample = False): - original_num = len(pcd.points) - target_samples = 1500 # 1000 - num_samples = min(target_samples, original_num) - - # 第一步:使用体素下采样快速减少点数量 - # voxel_size = 3 - if farthest_sample: - pcd_voxel = pcd.farthest_point_down_sample(num_samples=num_samples) - else: - pcd_voxel = pcd.voxel_down_sample(voxel_size) - down_num = len(pcd_voxel.points) - # print(f"original_num={original_num}, down_num={down_num}") - - # 第二步:仅在必要时进行最远点下采样 - if len(pcd_voxel.points) > target_samples and False: - pcd_downsampled = pcd_voxel.farthest_point_down_sample(num_samples=num_samples) - else: - pcd_downsampled = pcd_voxel - - return pcd_downsampled - -def down_obj_data_to_ply(weight_fix_out_obj_dir,weight_fix_out_ply_dir): - """""" - obj_file_list = [aa for aa in os.listdir(weight_fix_out_obj_dir) if aa.endswith(".obj")] - for obj_name in obj_file_list: - obj_path = os.path.join(weight_fix_out_obj_dir,obj_name) - - mesh_obj = read_mesh(obj_path) - - vertices = np.asarray(mesh_obj.vertices) - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(vertices) - voxel_size = 3 # 设置体素的大小,决定下采样的密度 - pcd_downsampled = down_sample(pcd, voxel_size) - ply_out_path = os.path.join(weight_fix_out_ply_dir,obj_name.replace(".obj",".ply")) - o3d.io.write_point_cloud(ply_out_path, pcd_downsampled) - print(ply_out_path,"下采样完成。") - -def compute_centroid_compact(pcd): - points = np.asarray(pcd.points) - centroid = np.mean(points, axis=0) - return centroid - -def compute_base_point(pcd): - points = np.asarray(pcd.points) - x_center = np.mean(points[:, 0]) - y_center = np.mean(points[:, 1]) - min_z = np.min(points[:, 2]) - return np.array([x_center, y_center, min_z]) - -import copy -def move_obj_to_compact_bounds(bounds_fix_out_dir,bounds_compact_out_dir,weight_fix_out_obj_dir,base_original_obj_dir,compact_obj_out_dir,dict_mesh_obj,dict_unplaced,placed_remove_dir,dict_bad,bad_dir,full_dir,dict_bounds_fix,dict_compact,dict_origin): - """""" - # obj_file_list = [aa for aa in os.listdir(weight_fix_out_obj_dir) if aa.endswith(".obj")] - obj_file_list = list(dict_mesh_obj.keys()) - ply_path_dict = {} - - # meshes = [] - # for ply_file_name in os.listdir(bounds_fix_out_dir): - for ply_file_name in dict_bounds_fix: - ply_dict_key = ply_file_name.split("=")[0] - ply_path_dict[ply_dict_key] = ply_file_name - for obj_name in obj_file_list: - obj_path = os.path.join(weight_fix_out_obj_dir, obj_name) - - # mesh_obj = read_mesh(obj_path, False) - mesh_obj = dict_mesh_obj[obj_name] - # mesh_obj = dict_origin[obj_origin_path] - - original_obj_pid_dir = base_original_obj_dir - obj_origin_path = os.path.join(original_obj_pid_dir, obj_name) - obj_origin = dict_origin[obj_origin_path] - # obj_origin = copy.deepcopy(dict_origin[obj_origin_path]) - - ply_name_pid = obj_name.replace(".obj","") - # ply_name = ply_path_dict[ply_name_pid] - ply_name = ply_path_dict.get(ply_name_pid,None) - print(ply_name_pid,ply_name) - if ply_name is None: - continue - print("move_obj_to_compact_bounds",ply_name,len(dict_unplaced)) - if not ply_name or ply_name in dict_unplaced: - print("unplaced",ply_name) - continue - ply_fix_path = os.path.join(bounds_fix_out_dir,ply_name) - ply_compact_path = os.path.join(bounds_compact_out_dir, ply_name) - # pcd_fix = o3d.io.read_point_cloud(ply_fix_path) - pcd_fix = dict_bounds_fix[ply_name] - - vertices = np.asarray(obj_origin.vertices) - pcd_origin = o3d.geometry.PointCloud() - pcd_origin.points = o3d.utility.Vector3dVector(vertices) - - # pcd_compact = o3d.io.read_point_cloud(ply_compact_path) - pcd_compact = dict_compact[ply_name] - - centroid_fix = compute_centroid_compact(pcd_fix) - centroid_compact = compute_centroid_compact(pcd_compact) - centroid_origin = compute_centroid_compact(pcd_origin) - displacement = centroid_compact - centroid_fix - # displacement = centroid_compact - centroid_origin - vertices = np.asarray(mesh_obj.vertices) - vertices_translated = vertices + displacement # 将位移应用到每个顶点 - mesh_obj.vertices = o3d.utility.Vector3dVector(vertices_translated) # 更新网格顶点 - - obj_pid = obj_name.split("_P")[0] - #compact_obj_pid_out_dir = os.path.join(compact_obj_out_dir,obj_pid) - compact_obj_pid_out_dir= compact_obj_out_dir - if not os.path.exists(compact_obj_pid_out_dir): - os.makedirs(compact_obj_pid_out_dir) - obj_path_compact = os.path.join(compact_obj_pid_out_dir,obj_name) - mesh_obj.compute_vertex_normals() - o3d.io.write_triangle_mesh(obj_path_compact, mesh_obj,write_triangle_uvs=True) - print(obj_path_compact, "移动后obj保存完成", displacement) - - # meshes.append(mesh_obj) - - #original_obj_pid_dir = os.path.join(base_original_obj_dir,obj_pid) - original_obj_pid_dir = base_original_obj_dir - for mtl in os.listdir(compact_obj_pid_out_dir): - if mtl.endswith(".mtl"): - if obj_pid in mtl: - mtl_path = os.path.join(compact_obj_pid_out_dir,mtl) - os.remove(mtl_path) - - mtl_name = None - tex_name = None - for file_name in os.listdir(original_obj_pid_dir): - if file_name.endswith(".mtl"): - if obj_pid in file_name: - mtl_name = file_name - if file_name.endswith(".jpg"): - if obj_pid in file_name: - tex_name = file_name - if file_name.endswith(".png"): - if obj_pid in file_name: - tex_name = file_name - - for file in os.listdir(original_obj_pid_dir): - #print(f"file{file}") - if file.endswith(".obj"): - continue - if obj_pid not in file: - continue - - origin_path = os.path.join(original_obj_pid_dir,file) - dis_path = os.path.join(compact_obj_pid_out_dir,file) - - if os.path.isfile(origin_path): - #print(f'origin_path{origin_path}') - #print(f'dis_path{dis_path}') - shutil.copy(origin_path,dis_path) - time.sleep(1) - #print("-"*50) - base_origin_obj_path = os.path.join(original_obj_pid_dir,obj_name) - #print(f"base_origin_obj_path{base_origin_obj_path}") - #print(f"obj_path_compact{obj_path_compact}") - update_obj_file(base_origin_obj_path, obj_path_compact) - - placed_remove_obj_path = os.path.join(placed_remove_dir, obj_name) - shutil.copy(base_origin_obj_path,placed_remove_obj_path) - os.remove(base_origin_obj_path) - - exist_obj_any = False - exist_obj = False - delete_mtl = False - for file_name in os.listdir(original_obj_pid_dir): - if file_name.endswith(".obj"): - if obj_pid in file_name: - exist_obj = True - exist_obj_any = True - - if not exist_obj_any: - delete_mtl = True - if not exist_obj: - delete_mtl = True - - if delete_mtl: - print("delete_mtl",mtl_name,tex_name) - if mtl_name!=None: - base_origin_mtl_path = os.path.join(original_obj_pid_dir,mtl_name) - placed_remove_mtl_path = os.path.join(placed_remove_dir, mtl_name) - shutil.copy(base_origin_mtl_path,placed_remove_mtl_path) - os.remove(base_origin_mtl_path) - - if tex_name!=None: - base_origin_tex_path = os.path.join(original_obj_pid_dir,tex_name) - placed_remove_tex_path = os.path.join(placed_remove_dir, tex_name) - shutil.copy(base_origin_tex_path,placed_remove_tex_path) - os.remove(base_origin_tex_path) - - """ - # 创建可视化窗口 - vis = o3d.visualization.Visualizer() - vis.create_window(window_name='模型展示') - - # 添加所有模型到场景 - for mesh in meshes: - vis.add_geometry(mesh) - - # 设置相机视角 - vis.get_render_option().mesh_show_back_face = True - vis.get_render_option().light_on = True - - # 运行可视化 - vis.run() - vis.destroy_window() - """ - - print(f"排版错误模型数量::{len(dict_bad)}") - for obj_name in dict_bad: - print("--错误模型名:", obj_name) - process_obj_files(original_obj_pid_dir,bad_dir,obj_name) - - print(f"排版剩余模型数量::{len(dict_unplaced)}") - for ply_file_name in dict_unplaced: - obj_name = ply_file_name.split("=")[0]+".obj" - print("--剩余模型名:", obj_name) - process_obj_files(original_obj_pid_dir,full_dir,obj_name) - -import json - -def extract_angles(R): - # 提取绕X、Y、Z轴的旋转角度(弧度) - rx_rad = np.arctan2(R[2, 1], R[2, 2]) # X轴旋转 - ry_rad = np.arcsin(-R[2, 0]) # Y轴旋转 - rz_rad = np.arctan2(R[1, 0], R[0, 0]) # Z轴旋转 - - # 将弧度转换为角度(度数) - rx_deg = np.degrees(rx_rad) - ry_deg = np.degrees(ry_rad) - rz_deg = np.degrees(rz_rad) - - return rx_deg, ry_deg, rz_deg - -def compute_mesh_center(vertices): - """ - 计算网格质心 - - 参数: - vertices: 顶点坐标数组,形状为(N, 3)的NumPy数组或列表 - - 返回: - centroid: 质心坐标的NumPy数组 [x, y, z] - """ - if len(vertices) == 0: - raise ValueError("顶点数组不能为空") - - n = len(vertices) # 顶点数量 - # 初始化坐标累加器 - sum_x, sum_y, sum_z = 0.0, 0.0, 0.0 - - # 遍历所有顶点累加坐标值 - for vertex in vertices: - sum_x += vertex[0] - sum_y += vertex[1] - sum_z += vertex[2] - - # 计算各坐标轴的平均值 - centroid = np.array([sum_x / n, sum_y / n, sum_z / n]) - return centroid - -import re -def extract_numbers_from_filename(filename): - """ - 从文件名中提取893333, 338908, 105043和x后面的数字 - """ - # 提取前两个下划线前的数字 - first_part = re.findall(r'^(\d+)_(\d+)', filename) - if first_part: - num1, num2 = first_part[0] - else: - num1, num2 = None, None - - # 提取P后面的数字 - p_number = re.findall(r'P(\d+)', filename) - num3 = p_number[0] if p_number else None - - # 提取x后面的数字 - x_number = re.findall(r'x(\d+)', filename) - num4 = x_number[0] if x_number else None - - return [num for num in [num1, num2, num3, num4] if num is not None] - -import requests -def move_obj_to_compact_bounds_json(bounds_fix_out_dir, bounds_compact_out_dir, weight_fix_out_obj_dir, - base_original_obj_dir, compact_obj_out_dir, dict_mesh_obj,dict_unplaced, - placed_remove_dir, dict_bad, bad_dir, full_dir,dict_best_angel,dict_bounds_fix, - dict_compact,dict_origin,dict_total_matrix,save_mesh,cache_type_setting_dir,batch_id, print_start_time,selected_machine,selected_mode,version): - """生成3D打印布局的JSON数据并保存为3DPrintLayout.json""" - # 创建符合3DPrintLayout规范的JSON数据结构 - layout_data = { - "summary": { - "version": version, - "homo_matrix": "Homogeneous Matrix", - "precision": 6, - "selected_machine": selected_machine, - "selected_mode": selected_mode - }, - "models": [] - } - - send_layout_data={ - "data": [], - "pre_complate_time": 0.0, - "pre_batch_id": batch_id, - "type_setting_start_time": print_start_time - } - - print("is_test=", is_test) - if is_test: - is_send_layout_data = False - else: - is_send_layout_data = True - # is_send_layout_data = False - - obj_file_list = list(dict_mesh_obj.keys()) - ply_path_dict = {} - meshes = [] - - original_obj_pid_dir = base_original_obj_dir - - # 构建PLY文件路径映射 - # for ply_file_name in os.listdir(bounds_fix_out_dir): - for ply_file_name in dict_bounds_fix: - ply_dict_key = ply_file_name.split("=")[0] - ply_path_dict[ply_dict_key] = ply_file_name - - for obj_name in obj_file_list: - ply_name_pid = obj_name.replace(".obj", "") - ply_name = ply_path_dict.get(ply_name_pid, None) - - if is_send_layout_data: - result = extract_numbers_from_filename(ply_name) - - if not ply_name or ply_name in dict_unplaced: - - if is_send_layout_data: - print_id = result[2] - order_id = result[0] - status = 0 - pid = result[1] - counts = result[3] - send_layout_data["data"].append({ - "print_id": print_id, - "order_id": order_id, - "status": status, - "pid":pid, - "counts":counts}) - - continue # 跳过未放置的模型 - - total_matrix = dict_total_matrix[obj_name] - - # if save_mesh: - # print("do save mesh here") - - flattened = total_matrix.flatten()[:16] - - matrix_4x4 = [ - [round(flattened[i], 6) for i in range(0, 4)], # 第1行 - [round(flattened[i], 6) for i in range(4, 8)], # 第2行 - [round(flattened[i], 6) for i in range(8, 12)], # 第3行 - [round(flattened[i], 6) for i in range(12, 16)] # 第4行 - ] - - layout_data["models"].append({ - "file_name": obj_name, - "transform": { - "homo_matrix": matrix_4x4 - } - }) - - if is_send_layout_data: - print_id = result[2] - order_id = result[0] - status = 1 - pid = result[1] - counts = result[3] - send_layout_data["data"].append({ - "print_id": print_id, - "order_id": order_id, - "status": status, - "pid":pid, - "counts":counts}) - - # 保存JSON文件 - # json_path = os.path.join(base_original_obj_dir, "3DPrintLayout.json") - json_path = os.path.join(base_original_obj_dir, f"{batch_id}.json") - - """ - if is_send_layout_data: - print(f"send_layout_data={send_layout_data}") - url = 'https://mp.api.suwa3d.com/api/printTypeSettingOrder/printTypeSettingOrderSuccess' - # url = 'http://127.0.0.1:8199/api/typeSettingPrintOrder/printTypeSettingOrderSuccess' - try: - response = requests.post(url, json.dumps(send_layout_data), timeout=30) - #写入文件中 log/request.txt - # with open('log/request.txt', 'w+') as f: - # f.write(json.dumps(send_layout_data, ensure_ascii=False, indent=2)) - # 检查响应状态码 - if response.status_code == 200: - try: - result = response.json() - print(f"请求成功,返回结果: {result}") - except ValueError as e: - print(f"响应不是有效的JSON格式: {e}") - print(f"响应内容: {response.text}") - else: - print(f"请求失败,状态码: {response.status_code}") - print(f"响应内容: {response.text}") - except requests.exceptions.Timeout: - print(f"请求超时: 连接 {url} 超过30秒未响应") - except requests.exceptions.ConnectionError as e: - print(f"连接错误: 无法连接到服务器 {url}, 错误信息: {e}") - except requests.exceptions.RequestException as e: - print(f"请求异常: {e}") - except Exception as e: - print(f"未知错误: {e}") - """ - - import re - json_str = json.dumps(layout_data, ensure_ascii=False, indent=2) - json_str = re.sub( - r'\[\s*(-?[\d.]+),\s+(-?[\d.]+),\s+(-?[\d.]+),\s+(-?[\d.]+)\s*\]', - r'[\1,\2,\3,\4]', - json_str - ) - with open(json_path, "w", encoding='utf-8') as f: - f.write(json_str) - - print(f"3D打印布局已保存至: {json_path}") - - print(f"排版错误模型数量::{len(dict_bad)}") - for obj_name in dict_bad: - print("--错误模型名:", obj_name) - process_obj_files(original_obj_pid_dir,bad_dir,obj_name) - - print(f"排版剩余模型数量::{len(dict_unplaced)}") - for ply_file_name in dict_unplaced: - obj_name = ply_file_name.split("=")[0]+".obj" - print("--剩余模型名:", obj_name) - process_obj_files(original_obj_pid_dir,full_dir,obj_name) - - if save_mesh: - transform_save(layout_data, original_obj_pid_dir, cache_type_setting_dir) - - """ - # 小打印机380*345,需要偏移-380,-345 - need_offset = True - for model in layout_data["models"]: - transform = model.get('transform', {}) - - homo_matrix = transform["homo_matrix"] # 获取存储的列表 - reconstructed_matrix = np.array(homo_matrix, dtype=np.float64) - - obj_name = model.get('file_name', '') - obj_path = os.path.join(original_obj_pid_dir, obj_name) - # 加载网格 - try: - mesh = o3d.io.read_triangle_mesh(obj_path, enable_post_processing=True) - if not mesh.has_vertices(): - print(f"警告: 网格无有效顶点 - {obj_path}") - continue - except Exception as e: - print(f"加载模型失败: {obj_path} - {e}") - continue - - original_vertices = np.asarray(mesh.vertices) - - transformed_vertices = custom_mesh_transform(original_vertices, reconstructed_matrix) - # 如果 need_offset 为 True,应用额外的偏移 - if need_offset: - # 应用偏移 (-380, -345, 0) - offset = np.array([-380, -345, 0]) - transformed_vertices += offset - print(f"已对模型 {obj_name} 应用偏移: {offset}") - - mesh.vertices = o3d.utility.Vector3dVector(transformed_vertices) - - meshes.append(mesh) - - # obj_path_arrange = os.path.join(original_obj_pid_dir, "arrange") - obj_path_arrange = cache_type_setting_dir - if not os.path.exists(obj_path_arrange): - os.mkdir(obj_path_arrange) - obj_path_arrange_obj = os.path.join(obj_path_arrange, obj_name) - print("obj_path_arrange_obj", obj_path_arrange_obj) - mesh.compute_vertex_normals() - o3d.io.write_triangle_mesh(obj_path_arrange_obj, mesh,write_triangle_uvs=True) - # """ - - return layout_data, send_layout_data - -#""" -def transform_save(layout_data, original_obj_pid_dir, cache_type_setting_dir): - print(f"original_obj_pid_dir={original_obj_pid_dir}, cache_type_setting_dir={cache_type_setting_dir}") - meshes = [] - # 小打印机380*345,需要偏移-380,-345 - need_offset = True - for model in layout_data["models"]: - transform = model.get('transform', {}) - - homo_matrix = transform["homo_matrix"] # 获取存储的列表 - reconstructed_matrix = np.array(homo_matrix, dtype=np.float64) - - obj_name = model.get('file_name', '') - obj_path = os.path.join(original_obj_pid_dir, obj_name) - # 加载网格 - try: - mesh = o3d.io.read_triangle_mesh(obj_path, enable_post_processing=True) - if not mesh.has_vertices(): - print(f"警告: 网格无有效顶点 - {obj_path}") - continue - except Exception as e: - print(f"加载模型失败: {obj_path} - {e}") - continue - - original_vertices = np.asarray(mesh.vertices) - - transformed_vertices = custom_mesh_transform(original_vertices, reconstructed_matrix) - # 如果 need_offset 为 True,应用额外的偏移 - if need_offset: - # 应用偏移 (-380, -345, 0) - offset = np.array([-380, -345, 0]) - transformed_vertices += offset - print(f"已对模型 {obj_name} 应用偏移: {offset}") - - mesh.vertices = o3d.utility.Vector3dVector(transformed_vertices) - - meshes.append(mesh) - - # obj_path_arrange = os.path.join(original_obj_pid_dir, "arrange") - obj_path_arrange = cache_type_setting_dir - if not os.path.exists(obj_path_arrange): - os.mkdir(obj_path_arrange) - obj_path_arrange_obj = os.path.join(obj_path_arrange, obj_name) - # print("obj_path_arrange_obj", obj_path_arrange_obj) - - mesh.compute_vertex_normals() - o3d.io.write_triangle_mesh(obj_path_arrange_obj, mesh, write_triangle_uvs=True) -#""" - -#""" -def transform_save2(layout_data, original_obj_pid_dir, cache_type_setting_dir): - print(f"original_obj_pid_dir={original_obj_pid_dir}, cache_type_setting_dir={cache_type_setting_dir}") - - # 确保输出目录存在 - os.makedirs(cache_type_setting_dir, exist_ok=True) - - # 小打印机380 * 345,需要偏移-380,-345 - need_offset = True - - for model in layout_data["models"]: - transform = model.get('transform', {}) - homo_matrix = transform["homo_matrix"] - reconstructed_matrix = np.array(homo_matrix, dtype=np.float64) - - obj_name = model.get('file_name', '') - obj_path = os.path.join(original_obj_pid_dir, obj_name) - - # 1. 加载原始网格 - try: - mesh = o3d.io.read_triangle_mesh(obj_path, enable_post_processing=True) - if not mesh.has_vertices(): - print(f"警告: 网格无有效顶点 - {obj_path}") - continue - except Exception as e: - print(f"加载模型失败: {obj_path} - {e}") - continue - - # 2. 应用顶点变换 - original_vertices = np.asarray(mesh.vertices) - transformed_vertices = custom_mesh_transform(original_vertices, reconstructed_matrix) - - if need_offset: - offset = np.array([-380, -345, 0]) - transformed_vertices += offset - print(f"已对模型 {obj_name} 应用偏移: {offset}") - - mesh.vertices = o3d.utility.Vector3dVector(transformed_vertices) - - # 3. 计算顶点法线 - mesh.compute_vertex_normals() - - # 4. 构建输出路径 - obj_path_arrange_obj = os.path.join(cache_type_setting_dir, obj_name) - print("保存路径:", obj_path_arrange_obj) - - # 5. 关键步骤:直接写入OBJ文件,只包含几何数据 - write_obj_without_new_materials(mesh, obj_path_arrange_obj, obj_path) - - post_process_obj_to_use_original_mtl(obj_path, obj_path_arrange_obj) - - print(f"模型 {obj_name} 处理完成,已保留原始材质") - -def write_obj_without_new_materials(mesh, output_obj_path, original_obj_path): - """ - 只写入几何数据(顶点、面、法线、UV),不生成新的材质文件 - """ - # 读取原始OBJ文件中的材质引用 - original_mtl_ref = get_original_mtl_reference(original_obj_path) - - # 写入新的OBJ文件 - with open(output_obj_path, 'w', encoding='utf-8') as f: - # 写入原始材质引用(如果存在) - # print(f"original_mtl_ref={original_mtl_ref}") - if original_mtl_ref: - f.write(f"mtllib {original_mtl_ref}\n") - - # 写入顶点 - vertices = np.asarray(mesh.vertices) - for v in vertices: - f.write(f"v {v[0]:.4f} {v[1]:.4f} {v[2]:.4f}\n") - - # 写入顶点法线 - if mesh.has_vertex_normals(): - normals = np.asarray(mesh.vertex_normals) - for n in normals: - f.write(f"vn {n[0]:.4f} {n[1]:.4f} {n[2]:.4f}\n") - - # 写入纹理坐标(UV) - # print(f"has_triangle_uvs={mesh.has_triangle_uvs}") - if mesh.has_triangle_uvs(): - uvs = np.asarray(mesh.triangle_uvs) - for uv in uvs: - f.write(f"vt {uv[0]:.4f} {uv[1]:.4f}\n") - - # 写入面 - triangles = np.asarray(mesh.triangles) - uv_indices = np.arange(len(triangles) * 3).reshape(-1, 3) if mesh.has_triangle_uvs() else None - - for i, tri in enumerate(triangles): - v1, v2, v3 = tri + 1 # OBJ索引从1开始 - - if mesh.has_vertex_normals() and mesh.has_triangle_uvs(): - # 顶点索引/纹理索引/法线索引 - uv1, uv2, uv3 = uv_indices[i] + 1 - f.write(f"f {v1}/{uv1}/{v1} {v2}/{uv2}/{v2} {v3}/{uv3}/{v3}\n") - elif mesh.has_vertex_normals(): - f.write(f"f {v1}//{v1} {v2}//{v2} {v3}//{v3}\n") - elif mesh.has_triangle_uvs(): - uv1, uv2, uv3 = uv_indices[i] + 1 - f.write(f"f {v1}/{uv1} {v2}/{uv2} {v3}/{uv3}\n") - else: - f.write(f"f {v1} {v2} {v3}\n") - -def get_original_mtl_reference(original_obj_path): - """ - 从原始OBJ文件中提取mtllib引用 - """ - if not os.path.exists(original_obj_path): - return None - - with open(original_obj_path, 'r', encoding='utf-8') as f: - for line in f: - if line.startswith('mtllib'): - return line.split()[1] # 返回mtllib后面的文件名 - return None - -def post_process_obj_to_use_original_mtl(original_obj_path, new_obj_path): - """ - 后处理OBJ文件,使其引用原始的MTL材质库而非新生成的。 - """ - original_mtl_name = None - original_dir = os.path.dirname(original_obj_path) - - # 读取原始OBJ文件,查找其使用的MTL材质库名称 - if os.path.exists(original_obj_path): - with open(original_obj_path, 'r', encoding='utf-8') as f: - for line in f: - line = line.strip() - if line.startswith('mtllib'): - original_mtl_name = line.split()[1] # 获取mtllib后面的文件名 - break - - # 如果找到了原始MTL引用,并且该MTL文件存在,则复制它到新位置 - if original_mtl_name: - original_mtl_path = os.path.join(original_dir, original_mtl_name) - new_dir = os.path.dirname(new_obj_path) - new_mtl_path = os.path.join(new_dir, original_mtl_name) - - # 复制MTL文件 - if os.path.exists(original_mtl_path): - shutil.copy2(original_mtl_path, new_mtl_path) - print(f"已复制材质文件: {original_mtl_path} -> {new_mtl_path}") - - # 同时复制MTL中引用的所有纹理图片 - copy_textures_referenced_in_mtl(original_mtl_path, original_dir, new_dir) - - # 读取新OBJ文件内容,修改mtllib行指向原始的材质库 - if os.path.exists(new_obj_path): - with open(new_obj_path, 'r', encoding='utf-8') as f: - content = f.readlines() - - # 修改内容:将mtllib行指向原始材质库 - with open(new_obj_path, 'w', encoding='utf-8') as f: - for line in content: - if line.startswith('mtllib'): - f.write(f'mtllib {original_mtl_name}\n') - else: - f.write(line) - -def copy_textures_referenced_in_mtl(mtl_path, original_dir, new_dir): - """ - 复制MTL文件中引用的所有纹理图片。 - """ - try: - with open(mtl_path, 'r', encoding='utf-8') as f: - for line in f: - line = line.strip() - # 查找纹理贴图定义(常见的贴图类型) - if any(line.startswith(keyword) for keyword in ['map_Kd', 'map_Ks', 'map_Ka', 'map_bump', 'map_d']): - parts = line.split() - if len(parts) >= 2: - tex_name = parts[1] - original_tex_path = os.path.join(original_dir, tex_name) - new_tex_path = os.path.join(new_dir, tex_name) - - if os.path.exists(original_tex_path): - shutil.copy2(original_tex_path, new_tex_path) - print(f"已复制纹理文件: {original_tex_path} -> {new_tex_path}") - except Exception as e: - print(f"处理材质文件 {mtl_path} 时出错: {e}") -#""" - -#""" -#""" - -def process_obj_files(original_obj_pid_dir,placed_remove_dir,obj_name): - """ - 处理OBJ文件及其相关资源文件的复制、更新和清理 - - 参数: - original_obj_pid_dir: 包含原始OBJ文件的目录 - placed_remove_dir: 用于存放移除文件的目录 - obj_name: 要处理的OBJ文件名 - """ - - base_origin_obj_path = os.path.join(original_obj_pid_dir,obj_name) - - # 从obj_name中提取PID(产品ID) - obj_pid = obj_name.split("_P")[0] - - # 查找相关的MTL和纹理文件 - mtl_name = None - tex_name = None - - for file_name in os.listdir(original_obj_pid_dir): - - if file_name.endswith(".mtl") and obj_pid in file_name: - mtl_name = file_name - - if (file_name.endswith(".jpg") or file_name.endswith(".png")) and obj_pid in file_name: - tex_name = file_name - - # 将原始OBJ文件移动到移除目录 - placed_remove_obj_path = os.path.join(placed_remove_dir, obj_name) - shutil.copy(base_origin_obj_path, placed_remove_obj_path) - os.remove(base_origin_obj_path) - - # 检查目录中是否还有其他OBJ文件 - exist_obj_any = False - exist_obj = False - - for file_name in os.listdir(original_obj_pid_dir): - if file_name.endswith(".obj"): - exist_obj_any = True - if obj_pid in file_name: - exist_obj = True - - # 确定是否需要删除MTL和纹理文件 - delete_mtl = not exist_obj_any or not exist_obj - - # 如果确定要删除,移动MTL和纹理文件 - if delete_mtl: - if mtl_name: - base_origin_mtl_path = os.path.join(original_obj_pid_dir, mtl_name) - placed_remove_mtl_path = os.path.join(placed_remove_dir, mtl_name) - shutil.copy(base_origin_mtl_path, placed_remove_mtl_path) - os.remove(base_origin_mtl_path) - - if tex_name: - base_origin_tex_path = os.path.join(original_obj_pid_dir, tex_name) - placed_remove_tex_path = os.path.join(placed_remove_dir, tex_name) - shutil.copy(base_origin_tex_path, placed_remove_tex_path) - os.remove(base_origin_tex_path) - -def update_obj_file(original_obj_path,compact_obj_path): - """""" - with open(original_obj_path, "r") as f: - lines_original = f.readlines() - mtllib_name = None - mat_name = None - for line in lines_original: - if line.startswith("mtllib"): - mtllib_name = line.split(" ")[1] - elif line.startswith("usemtl"): - mat_name = line.split(" ")[1] - if mtllib_name and mat_name: - break - with open(compact_obj_path, "r") as f: - lines = f.readlines() - new_lines = [] - for line2 in lines: - if line2.startswith("mtllib"): - line2 = f"mtllib {mtllib_name}\n" # 替换为原始 MTL 文件路径 - elif line2.startswith("usemtl"): - line2 = f"usemtl {mat_name}\n" # 替换为原始贴图路径 - new_lines.append(line2) - with open(compact_obj_path, "w") as f: - f.writelines(new_lines) - -def import_and_process_obj(input_obj_file, output_obj_file, xiong_zhang): - # 清除现有对象 - bpy.ops.object.select_all(action='SELECT') - bpy.ops.object.delete(use_global=False) - - # 导入OBJ文件 - bpy.ops.wm.obj_import(filepath=input_obj_file) - bpy.context.object.name = 'body' - - parent_dir = os.path.dirname(input_obj_file) - # 查找 parent_dir 目录中的 .jpg 文件,并设置为材质的贴图 - for file in os.listdir(parent_dir): - if file.endswith('.jpg'): - texture_path = os.path.join(parent_dir, file) - break - materials = bpy.data.materials - for material in materials: - # 确保材质使用节点树 - if not material.use_nodes: - material.use_nodes = True - - node_tree = material.node_tree - texture_node = node_tree.nodes.new(type='ShaderNodeTexImage') - texture_node.image = bpy.data.images.load(texture_path) - - bsdf_node = node_tree.nodes.get('Principled BSDF') - if bsdf_node: - node_tree.links.new(bsdf_node.inputs['Base Color'], texture_node.outputs['Color']) - - bpy.ops.wm.obj_export(filepath=output_obj_file) - -def pass_for_min_dis(bounds_fix_out_dir,bounds_compact_out_dir, placed_models, dict_unplaced,dict_bounds_fix, dict_compact): - """ - for ply in os.listdir(bounds_fix_out_dir): - original_ply_path = os.path.join(bounds_fix_out_dir,ply) - dis_ply_path = os.path.join(bounds_compact_out_dir,ply) - shutil.copy(original_ply_path,dis_ply_path) - """ - for ply in dict_bounds_fix: - dict_compact[ply] = dict_bounds_fix[ply] - - pcd_all = [] - name_list = [] - model_list = [] - - for model in placed_models: - ply_origin_path = os.path.join(bounds_fix_out_dir,model['name']) - # pcd = o3d.io.read_point_cloud(ply_origin_path) - pcd = dict_bounds_fix[model['name']] - pcd_all.append(pcd) - name_list.append(model['name']) - model_list.append(model) - - for idx, pcd in enumerate(pcd_all): - y = model_list[idx]['position'][1] - dx = model_list[idx]['dimensions'][0] - dy = model_list[idx]['dimensions'][1] - # print("pass_for_min_dis", name_list[idx], y, dy) - - delta_y = 20 - # safe_y = y - delta_y - safe_y = y - dy - delta_y - min_y = 0 - if safe_y < min_y: - name = name_list[idx] - print("fail to place (x=0)", name_list[idx], y, dy) - dict_unplaced[name]=name - -def bake_textures(obj): - # 设置烘焙参数 - bpy.context.scene.render.engine = 'CYCLES' - bpy.context.scene.cycles.bake_type = 'DIFFUSE' - - # 创建贴图图像 - image = bpy.data.images.new("BakedTexture", 1024, 1024) - - # 执行烘焙 - bpy.ops.object.bake( - type='DIFFUSE', - pass_filter={'COLOR'}, - filepath=image.filepath - ) - - return image - -if __name__ == '__main__': - out_dir = "/data/datasets_20t/type_setting_test_data/" - weight_fix_out_obj_dir = f"{out_dir}/print_weight_fix_data_obj" - weight_fix_out_ply_dir = f"{out_dir}/data/datasets_20t/type_setting_test_data/print_weight_fix_data_ply" - print_factory_type_dir="/root/print_factory_type" - base_original_obj_dir="{print_factory_type_dir}/8/" - if not os.path.exists(weight_fix_out_ply_dir): - os.makedirs(weight_fix_out_ply_dir) - bounds_fix_out_dir = f"{out_dir}/print_bounds_fix_data" - bounds_compact_out_dir = f"{out_dir}/print_bounds_compact_data" - compact_obj_out_dir = f"{out_dir}//print_compact_obj" - if not os.path.exists(bounds_fix_out_dir): - os.mkdir(bounds_fix_out_dir) - if not os.path.exists(bounds_compact_out_dir): - os.makedirs(bounds_compact_out_dir) - if not os.path.exists(compact_obj_out_dir): - os.makedirs(compact_obj_out_dir) - - move_obj_to_compact_bounds(bounds_fix_out_dir,bounds_compact_out_dir,weight_fix_out_obj_dir,base_original_obj_dir,compact_obj_out_dir) \ No newline at end of file diff --git a/print_setting_run.py b/print_setting_run.py deleted file mode 100644 index 4b303ab..0000000 --- a/print_setting_run.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- coding: utf-8 -*- -import pandas as pd -from threading import Thread -from PyQt5.QtWidgets import QApplication, QMainWindow,QMessageBox -from print_setting_ui import Ui_MainWindow -from PyQt5.QtGui import QFont -from PySide2.QtCore import Signal,QObject -from PyQt5.QtCore import QThread, pyqtSignal, QProcess -import sys -import os -import warnings -import time -import time -from PyQt5.QtWidgets import (QApplication, QWidget, QPushButton, QLabel, - QVBoxLayout, QFileDialog, QMessageBox) -from threading import Thread -from print_factory_type_setting_obj_run import print_type_setting_obj - -warnings.filterwarnings('ignore') -pd.set_option('display.width', None) - - -class MySignals(QObject): - text_print = Signal(str) - update_table = Signal(str) - - -class MyMainForm(QMainWindow, Ui_MainWindow): - def __init__(self,parent=None): - super(MyMainForm, self).__init__(parent) - self.setupUi(self) - self.folder_path = "" - self.cache_path = "" - self.pushButton.clicked.connect(self.on_select_folder) - self.pushButton_2.clicked.connect(self.on_run_clicked) - self.pushButton_3.clicked.connect(self.on_open_output_clicked) - self.global_ms = MySignals() - self.global_ms.text_print.connect(self.printToGui) - - def printToGui(self,text): - n_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - self.textBrowser.append(n_time+" "+str(text)) - - def on_select_folder(self): - folder = QFileDialog.getExistingDirectory(self, "选择文件夹") - if folder: - self.folder_path = folder - #self.folder_path_label.setText(f"📂 当前选择文件夹: {folder}") - #self.run_status_label.setText("") - self.cache_path = folder + "_arrange" - os.makedirs(self.cache_path, exist_ok=True) - - def on_run_clicked(self): - - def threadFunc1(): - if not self.folder_path: - #self.run_status_label.setText("❗请先选择一个文件夹再执行!") - return - - # if hasattr(self, 'worker') and self.worker.isRunning(): - # self.run_status_label.setText("⚠️ 正在执行中,请稍候...") - # return - #self.run_btn.setEnabled(False) - #self.run_status_label.setText("🚀 程序正在运行,请稍候...") - - print_type_setting_obj( - base_original_obj_dir=self.folder_path, - cache_type_setting_dir=self.cache_path, - show_chart=False - ) - - #self.run_status_label.setText("✅ 排版完成!") - - thread = Thread(target=threadFunc1) - thread.start() - - def open_file_cross_platform(self, path): - if not os.path.exists(path): - print("路径不存在!") - return - if sys.platform.startswith('win'): - os.startfile(path) - elif sys.platform.startswith('darwin'): - QProcess.startDetached("open", [path]) - else: - QProcess.startDetached("xdg-open", [path]) - - def on_open_output_clicked(self): - output_path = os.path.join(self.cache_path, "print_compact_obj") - if os.path.exists(output_path): - self.open_file_cross_platform(output_path) - else: - self.run_status_label.setText("⚠️ 输出文件夹不存在!") - - - def douyin_spider_go(self): - """下载抖音视频""" - def threadFunc1(): - print("开始下载") - self.load_chrome_video() - thread = Thread(target=threadFunc1) - thread.start() - #thread.run() - - def open_config_dir(self): - """打开文件夹""" - def threadFunc1(): - try: - start_directory = os.path.join(self.dir_base) - os.startfile(start_directory) - except RecursionError: - print("打开配置文件夹失败。") - thread = Thread(target=threadFunc1) - thread.start() - - - - - -if __name__ == '__main__': - #multiprocessing.freeze_support() - app = QApplication(sys.argv) - myWin = MyMainForm() - myWin.show() - sys.exit(app.exec_()) - - diff --git a/print_setting_ui.py b/print_setting_ui.py deleted file mode 100644 index d3c139f..0000000 --- a/print_setting_ui.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'dou_spider_ui.ui' -# -# Created by: PyQt5 UI code generator 5.15.4 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.resize(300, 150) - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName("centralwidget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) - self.verticalLayout.setObjectName("verticalLayout") - #self.folder_path_label = QLabel("📁 请选择要排版的文件夹") - #self.folder_path_label.setWordWrap(True) - # 按钮布局 - self.pushButton = QtWidgets.QPushButton(self.centralwidget) - self.pushButton.setObjectName("pushButton") - self.verticalLayout.addWidget(self.pushButton) - - self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget) - self.pushButton_2.setObjectName("pushButton_2") - self.verticalLayout.addWidget(self.pushButton_2) - - self.pushButton_3 = QtWidgets.QPushButton(self.centralwidget) - self.pushButton_3.setObjectName("pushButton_3") - self.verticalLayout.addWidget(self.pushButton_3) - - MainWindow.setCentralWidget(self.centralwidget) - self.statusbar = QtWidgets.QStatusBar(MainWindow) - self.statusbar.setObjectName("statusbar") - MainWindow.setStatusBar(self.statusbar) - - self.retranslateUi(MainWindow) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "自动排版工具")) - self.pushButton.setText(_translate("MainWindow", "选择文件夹")) - self.pushButton_2.setText(_translate("MainWindow", "开始自动排版")) - self.pushButton_3.setText(_translate("MainWindow", "打开排版好的文件夹")) - diff --git a/print_show_weight_max_obj.py b/print_show_weight_max_obj.py index 695094e..61b34a1 100644 --- a/print_show_weight_max_obj.py +++ b/print_show_weight_max_obj.py @@ -17,10 +17,7 @@ from multiprocessing import Pool, RawArray import ctypes import itertools -from get_lowest_position_of_center_ext import get_lowest_position_of_center_ext -from get_lowest_position_of_center_ext import get_lowest_position_of_center_ext2 -from get_lowest_position_of_center_ext import get_lowest_position_of_center_ext3 -from get_lowest_position_of_center_ext import get_lowest_position_of_center_net +from compute_print_net import down_sample def make_pcd_plane(): # 创建Y=0的平面点云 @@ -360,22 +357,6 @@ def parallel_rotation4(points, angle_step=4): #""" -def read_mesh(obj_path, simple=True): - mesh_obj = o3d.io.read_triangle_mesh(obj_path, enable_post_processing=True) # - return mesh_obj - if not simple: - return mesh_obj - original_triangles = len(mesh_obj.triangles) - target_triangles = original_triangles if original_triangles <= 10000 else 10000 - if original_triangles > 10000: - mesh_obj = mesh_obj.simplify_quadric_decimation( - target_number_of_triangles=target_triangles, - maximum_error=0.0001, - boundary_weight=1.0 - ) - - return mesh_obj - def compute_mesh_center(vertices): """ 计算网格质心 @@ -403,28 +384,6 @@ def compute_mesh_center(vertices): centroid = np.array([sum_x / n, sum_y / n, sum_z / n]) return centroid -def down_sample(pcd, voxel_size, farthest_sample = False): - original_num = len(pcd.points) - target_samples = 1500 # 1000 - num_samples = min(target_samples, original_num) - - # 第一步:使用体素下采样快速减少点数量 - # voxel_size = 3 - if farthest_sample: - pcd_voxel = pcd.farthest_point_down_sample(num_samples=num_samples) - else: - pcd_voxel = pcd.voxel_down_sample(voxel_size) - down_num = len(pcd_voxel.points) - # print(f"original_num={original_num}, down_num={down_num}") - - # 第二步:仅在必要时进行最远点下采样 - if len(pcd_voxel.points) > target_samples and False: - pcd_downsampled = pcd_voxel.farthest_point_down_sample(num_samples=num_samples) - else: - pcd_downsampled = pcd_voxel - - return pcd_downsampled - def get_lowest_position_of_center(obj_path,voxel_size,dict_origin,total_matrix): mesh_obj = read_mesh(obj_path) @@ -728,153 +687,6 @@ def axis_angle_to_rotation_matrix(axis, angle): cos_a + axis[2]**2*(1-cos_a)] ]) -def arrange_box_correctly(obj_transformed, voxel_size,total_matrix): - - vertices = np.asarray(obj_transformed.vertices) - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(vertices) - - # 降采样与特征计算 - pcd_downsampled = down_sample(pcd, voxel_size) - - original_num = len(pcd.points) - target_samples = 1000 - - points = np.asarray(pcd_downsampled.points) - cov = np.cov(points.T) - - center = obj_transformed.get_center() - - # 特征分解与方向约束(关键修改点) - eigen_vals, eigen_vecs = np.linalg.eigh(cov) - max_axis = eigen_vecs[:, np.argmax(eigen_vals)] - - # arrow = o3d.geometry.TriangleMesh.create_arrow(0.05, 0.1) - # arrow.translate(center) - # o3d.visualization.draw_geometries([obj_transformed, arrow]) - - # print("max_axis", max_axis) - # 强制主方向向量X分量为正(指向右侧) - if max_axis[0] < 0 or (max_axis[0] == 0 and max_axis[1] < 0): - max_axis = -max_axis - - target_dir = np.array([1, 0]) # 目标方向为X正轴 - current_dir = max_axis[:2] / np.linalg.norm(max_axis[:2]) - dot_product = np.dot(current_dir, target_dir) - - # print("dot_product", dot_product) - if dot_product < 0.8: # 阈值控制方向敏感性(建议0.6~0.9) - max_axis = -max_axis # 强制翻转方向 - - # 计算旋转角度 - angle_z = np.arctan2(max_axis[1], max_axis[0]) % (2 * np.pi) - - - if max_axis[0] <= 0 and max_axis[1] <= 0: - angle_z += np.pi - - # print("max_axis2", max_axis, angle_z / np.pi * 180 % 360) - - # angle_z = 0 - R = o3d.geometry.get_rotation_matrix_from_axis_angle([0, 0, -angle_z]) - - T = np.eye(4) - T[:3, :3] = R - T[:3, 3] = center - R.dot(center) # 保持中心不变 - obj_transformed.transform(T) - - total_matrix = T @ total_matrix - - #arrow = o3d.geometry.TriangleMesh.create_arrow(0.05, 0.1) - #arrow.translate(center) - #o3d.visualization.draw_geometries([obj_transformed, arrow]) - - return obj_transformed, total_matrix - -def get_new_bbox(obj_transformed_second,obj_name,weight_fix_out_dir,weight_fix_out_ply_dir,voxel_size,show_chart,dict_fix,compact_min_dis,total_matrix): - - # 计算点云的边界 - points = np.asarray(obj_transformed_second.vertices) - - min_bound = np.min(points, axis=0) # 获取点云的最小边界 - max_bound = np.max(points, axis=0) # 获取点云的最大边界 - - # 确保包围盒的Y坐标不低于0 - min_bound[2] = max(min_bound[2], 0) # 确保Y坐标的最小值不低于0 - - # 重新计算包围盒的中心和半长轴 - bbox_center = (min_bound + max_bound) / 2 # 计算包围盒的中心点 - bbox_extent = (max_bound - min_bound) # 计算包围盒的半长轴(尺寸) - - # 创建包围盒,确保尺寸正确 - new_bbox = o3d.geometry.OrientedBoundingBox(center=bbox_center, - R=np.eye(3), # 旋转矩阵,默认没有旋转 - extent=bbox_extent) - # 获取包围盒的长、宽和高 - x_length = round(bbox_extent[0],3) # X 方向的长 - y_length = round(bbox_extent[1],3) # Y 方向的宽 - z_length = round(bbox_extent[2],3) # Z 方向的高 - bbox_points = np.array([ - [min_bound[0], min_bound[1], min_bound[2]], - [max_bound[0], min_bound[1], min_bound[2]], - [max_bound[0], max_bound[1], min_bound[2]], - [min_bound[0], max_bound[1], min_bound[2]], - [min_bound[0], min_bound[1], max_bound[2]], - [max_bound[0], min_bound[1], max_bound[2]], - [max_bound[0], max_bound[1], max_bound[2]], - [min_bound[0], max_bound[1], max_bound[2]] - ]) - first_corner = bbox_points[2] - translation_vector = -first_corner - # start_time = time.time() - obj_transformed_second.translate(translation_vector) - - T_trans = np.eye(4) - T_trans[:3, 3] = translation_vector # 设置平移分量 [2,3](@ref) - total_matrix = T_trans @ total_matrix # 矩阵乘法顺序:最新变换左乘[4,5](@ref) - - new_bbox.translate(translation_vector) - # print("get_new_bbox1",time.time()-start_time) - - vertices = np.asarray(obj_transformed_second.vertices) - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(vertices) - - ply_print_pid = obj_name.replace(".obj","") - ply_name = f"{ply_print_pid}={z_length}+{y_length}+{x_length}.ply" - ply_out_path = os.path.join(weight_fix_out_ply_dir,ply_name) - # o3d.io.write_point_cloud(ply_out_path, pcd_downsampled) - # o3d.io.write_point_cloud(ply_out_path, pcd) - - if compact_min_dis: - original_num = len(pcd.points) - target_samples = 1500 # 1000 - num_samples = min(target_samples, original_num) - start_time = time.time() - pcd_downsampled = down_sample(pcd, voxel_size, False) - # print("down_sample time =",time.time()-start_time) - dict_fix[ply_name] = pcd_downsampled - else: - dict_fix[ply_name] = pcd - - # print("dict_fix write",ply_name) - # print("voxel_down_sample&&write_point_cloud",time.time()-start_time) - - if show_chart: - # 创建包围盒的轮廓(线框) - new_bbox_lines = o3d.geometry.LineSet.create_from_oriented_bounding_box(new_bbox) - new_bbox_lines.paint_uniform_color([1, 0, 0]) # 红色 - - #创建坐标系 - coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=2, origin=[0, 0, 0]) - - # 创建Y=0的平面点云 - pcd_plane = make_pcd_plane() - # 可视化点云和边界框 - o3d.visualization.draw_geometries([obj_transformed_second, coordinate_frame, pcd_plane, new_bbox_lines]) - - return obj_transformed_second, total_matrix - def load_obj_data(get_pid,base_out_dir): """下载obj文件""" access_key_id = os.getenv('OSS_TEST_ACCESS_KEY_ID', 'LTAI5tBWbfkZntfJij4Fg9gz') @@ -910,130 +722,6 @@ def load_obj_data(get_pid,base_out_dir): # 如果迭代器没有返回任何对象,那么表示该"文件夹"不存在 print("ossFolder does not exist.",get_pid) -def custom_mesh_transform(vertices, transform_matrix): - """ - 手动实现网格变换:对每个顶点应用齐次变换矩阵 - 参数: - vertices: 网格顶点数组 (N, 3) - transform_matrix: 4x4 齐次变换矩阵 - 返回: - 变换后的顶点数组 (N, 3) - """ - # 1. 顶点转齐次坐标 (N, 3) → (N, 4) - homogeneous_vertices = np.hstack((vertices, np.ones((vertices.shape[0], 1)))) - - # 2. 应用变换矩阵:矩阵乘法 (4x4) * (4xN) → (4xN) - transformed_homogeneous = transform_matrix @ homogeneous_vertices.T - - # 3. 转回非齐次坐标 (3xN) → (N, 3) - transformed_vertices = transformed_homogeneous[:3, :].T - return transformed_vertices - -def make_bbox_for_print(base_out_dir,weight_fix_out_dir,weight_fix_out_ply_dir,show_chart,dict_bad, dict_best_angel,dict_fix,dict_origin,dict_origin_real, compact_min_dis,dict_total_matrix): - """获取需要的盒子大小""" - # 加载点云数据 - start_time1 = time.time() - obj_id_list = [aa.split(".o")[0] for aa in os.listdir(base_out_dir) if aa.endswith(".obj")] - #print(obj_id_list) - #print(len(obj_id_list)) - #random.shuffle(obj_id_list) - obj_id_list = obj_id_list - #print(obj_id_list) - voxel_size = 3 # 设置体素的大小,决定下采样的密度 - - #for pid in tqdm(obj_id_list,desc="get new bbox"): - dict_mesh_obj = {} - for pid_t_y in obj_id_list: - start_time2 = time.time() - obj_name = pid_t_y+".obj" - obj_path = os.path.join(base_out_dir,obj_name) - - total_matrix = np.eye(4) - #放置最大接触面 - # obj_transformed, total_matrix = get_lowest_position_of_center(obj_path,voxel_size,dict_origin,total_matrix) - - mesh_obj = read_mesh(obj_path) - - # dict_origin_real[obj_path] = copy.deepcopy(mesh_obj) - dict_origin[obj_path] = copy.deepcopy(mesh_obj) - - start_time3 = time.time() - total_matrix, z_min= get_lowest_position_of_center_ext(obj_path, total_matrix) - # print("get_lowest_position_of_center_ext time", time.time()-start_time3) - # print(f"total_matrix={total_matrix}") - print(f"z_min={z_min}") - printId = "" - match = re.search(r"P(\d+)", obj_name) # 匹配 "P" 后的连续数字 - if match: - printId = match.group(1) - # print("printId", printId) - - # total_matrix, z_mean_min = get_lowest_position_of_center_net(printId, total_matrix) - - # print("total_matrix=", total_matrix) - - original_vertices = np.asarray(mesh_obj.vertices) - transformed_vertices = custom_mesh_transform(original_vertices, total_matrix) - mesh_obj.vertices = o3d.utility.Vector3dVector(transformed_vertices) - - # print("dict_origin[] obj_path=", obj_path) - # dict_origin[obj_path] = mesh_obj - - obj_transformed = copy.deepcopy(mesh_obj) - translation = total_matrix[:3, 3] - # print("make_bbox_for_print0", obj_name, translation) - - if obj_transformed is None: - dict_bad[obj_name]=obj_name - # print(len(dict_bad)) - # print(obj_name) - # 记录错误文件 - error_log = os.path.join(base_out_dir, "error_files.txt") - with open(error_log, 'a') as f: - f.write(f"{obj_path}\n") - print(f"Skipping invalid file: {obj_path}") - continue - - start_time3 = time.time() - best_angle_x, best_angle_y, best_angle_z, z_mean_min = get_lowest_position_of_center_ext3(mesh_obj, obj_path,voxel_size) - # print("get_lowest_position_of_center_ext2 time", time.time()-start_time3) - # print("best_angle=", best_angle_x, best_angle_y, best_angle_z, z_mean_min) - dict_best_angel[obj_name] = [int(round(best_angle_x)), int(round(best_angle_y)), int(round(best_angle_z))] - - start_time3 = time.time() - #将点云摆正和X轴平衡 - obj_transformed_second,total_matrix = arrange_box_correctly(obj_transformed,voxel_size,total_matrix) - # print("arrange_box_correctly time", time.time()-start_time3) - """ - # 创建可视化窗口 - vis = o3d.visualization.Visualizer() - vis.create_window(window_name='模型展示') - - # 添加所有模型到场景 - vis.add_geometry(obj_transformed_second) - - # 设置相机视角 - vis.get_render_option().mesh_show_back_face = True - vis.get_render_option().light_on = True - - # 运行可视化 - vis.run() - vis.destroy_window() - #""" - - #print("摆正后的obj") - #o3d.visualization.draw_geometries([obj_transformed_second, ]) - start_time3 = time.time() - mesh_obj,total_matrix = get_new_bbox(obj_transformed_second,obj_name,weight_fix_out_dir,weight_fix_out_ply_dir,voxel_size,show_chart,dict_fix,compact_min_dis,total_matrix) - dict_mesh_obj[obj_name] = mesh_obj - # print("get_new_bbox time", time.time()-start_time3) - - dict_total_matrix[obj_name] = total_matrix - print(f"make_bbox_for_print {obj_name} time={time.time()-start_time2}") - - print(f"make_bbox_for_print total_time={time.time()-start_time1}") - - return dict_mesh_obj import re def copy_obj_2x(base_obj_dir): obj_list = [aa for aa in os.listdir(base_obj_dir) if aa.endswith(".obj")] diff --git a/print_type_setting_gui.py b/print_type_setting_gui.py deleted file mode 100644 index 905058f..0000000 --- a/print_type_setting_gui.py +++ /dev/null @@ -1,346 +0,0 @@ -import sys -import os -from PyQt5.QtWidgets import (QApplication, QWidget, QPushButton, QLabel, - QVBoxLayout, QFileDialog, QMessageBox, - QRadioButton, QHBoxLayout, QButtonGroup, QGroupBox) -from PyQt5.QtCore import QProcess, QTimer -from threading import Thread -import print_factory_type_setting_obj_run - - -class AutoLayoutApp(QWidget): - def __init__(self): - super().__init__() - self.small_model_temp = False - self.setWindowTitle("自动排版工具") - self.setGeometry(200, 200, 600, 380) # 增加窗口高度以容纳新控件 - self.dw = print_factory_type_setting_obj_run - self.folder_path = "" - self.cache_path = "" - self.process = None - if self.small_model_temp: - self.selected_mode = "紧凑" # 默认排版模式 - self.output_format = "模型" # 默认输出格式 - self.selected_machine = "小机型" # 默认机型 - else: - self.selected_mode = "标准" # 默认排版模式 - self.output_format = "JSON" # 默认输出格式 - self.selected_machine = "大机型" # 默认机型 - self.is_running = False # 跟踪排版状态 - - self.init_ui() - - def init_ui(self): - layout = QVBoxLayout() - - # ===== 机型选择区域(移到最上面) ===== - machine_group = QGroupBox("机型选择") - machine_layout = QHBoxLayout() - - # 创建机型选择按钮组 - self.machine_group = QButtonGroup(self) - - # 创建两种机型选项 - self.large_machine = QRadioButton("大机型(600 * 500 * 300)") - self.small_machine = QRadioButton("小机型(380 * 345 * 250)") - - if self.small_model_temp: - self.large_machine.setEnabled(False) - self.small_machine.setChecked(True) # 默认选中 - else: - self.large_machine.setChecked(True) # 默认选中 - - # 添加到按钮组 - self.machine_group.addButton(self.large_machine, 1) - self.machine_group.addButton(self.small_machine, 2) - - # 连接信号 - self.machine_group.buttonToggled.connect(self.on_machine_changed) - - # 添加到布局 - machine_layout.addWidget(self.large_machine) - machine_layout.addWidget(self.small_machine) - machine_group.setLayout(machine_layout) - layout.addWidget(machine_group) - - # ===== 排版模式选择区域(改为QGroupBox) ===== - mode_group = QGroupBox("排版模式") - mode_layout = QHBoxLayout() - - # 创建单选按钮组 - self.mode_group = QButtonGroup(self) - - # 创建三种排版模式选项 - self.standard_mode = QRadioButton("标准模式(适合规整模型组)") - self.compact_mode = QRadioButton("紧凑模式(复杂度较高))") - self.advanced_mode = QRadioButton("高级模式(复杂度最高)") - self.advanced_mode.setVisible(False) - - if self.small_model_temp: - self.standard_mode.setEnabled(False) - self.compact_mode.setChecked(True) # 默认选中 - else: - self.standard_mode.setChecked(True) # 默认选中 - - # 添加到按钮组(确保互斥选择) - self.mode_group.addButton(self.standard_mode, 1) - self.mode_group.addButton(self.compact_mode, 2) - self.mode_group.addButton(self.advanced_mode, 3) - - # 连接信号 - self.mode_group.buttonToggled.connect(self.on_mode_changed) - - # 添加到布局 - mode_layout.addWidget(self.standard_mode) - mode_layout.addWidget(self.compact_mode) - mode_layout.addWidget(self.advanced_mode) - mode_group.setLayout(mode_layout) - layout.addWidget(mode_group) - - # ==== 输出格式选择区域 ==== - format_group = QGroupBox("输出格式") - format_layout = QHBoxLayout() - - # 创建输出格式按钮组 - self.format_group = QButtonGroup(self) - - # 创建两种输出格式选项 - self.json_format = QRadioButton("JSON格式") - self.model_format = QRadioButton("模型格式") - - if self.small_model_temp: - self.json_format.setEnabled(False) - self.model_format.setChecked(True) # 默认选中 - else: - self.json_format.setChecked(True) # 默认选中 - - # 添加到按钮组 - self.format_group.addButton(self.json_format, 1) - self.format_group.addButton(self.model_format, 2) - - # 连接信号 - self.format_group.buttonToggled.connect(self.on_format_changed) - - # 添加到布局 - format_layout.addWidget(self.json_format) - format_layout.addWidget(self.model_format) - format_group.setLayout(format_layout) - layout.addWidget(format_group) - - # ===== 原有UI元素 ===== - self.folder_path_label = QLabel(" 请选择要排版的文件夹") - self.folder_path_label.setWordWrap(True) - - self.run_status_label = QLabel("") - - layout.addWidget(self.folder_path_label) - layout.addWidget(self.run_status_label) - - self.select_folder_btn = QPushButton("选择文件夹") - self.select_folder_btn.clicked.connect(self.on_select_folder) - layout.addWidget(self.select_folder_btn) - - # 按钮布局(运行按钮和预览按钮在同一行) - buttons_layout = QHBoxLayout() - - self.run_btn = QPushButton("开始自动排版") - self.run_btn.clicked.connect(self.on_run_clicked) - buttons_layout.addWidget(self.run_btn) - - # ===== 新增预览按钮 ===== - self.preview_btn = QPushButton("预览排版结果") - self.preview_btn.clicked.connect(self.on_preview_clicked) - self.preview_btn.setEnabled(False) # 初始禁用,排版完成后才可用 - buttons_layout.addWidget(self.preview_btn) - - layout.addLayout(buttons_layout) - - self.open_output_btn = QPushButton("打开排版好的文件夹") - self.open_output_btn.clicked.connect(self.on_open_output_clicked) - layout.addWidget(self.open_output_btn) - - self.setLayout(layout) - - def on_mode_changed(self, button, checked): - """处理排版模式选择变化""" - if checked: - # self.selected_mode = button.text().replace("模式", "") - self.selected_mode = button.text()[:2] - self.run_status_label.setText(f"已选择: {self.selected_mode} 模式") - - def on_format_changed(self, button, checked): - """处理输出格式选择变化""" - if checked: - self.output_format = button.text().replace("格式", "") - self.run_status_label.setText(f"输出格式: {self.output_format}") - - def on_machine_changed(self, button, checked): - """处理机型选择变化""" - if checked: - self.selected_machine = button.text().split("(")[0] - self.run_status_label.setText(f"已选择: {self.selected_machine}") - - def on_select_folder(self): - folder = QFileDialog.getExistingDirectory(self, "选择文件夹") - if folder: - self.folder_path = folder - self.folder_path_label.setText(f" 当前选择文件夹: {folder}") - self.run_status_label.setText("") - self.cache_path = folder + "_arrange" - # self.cache_path = os.path.join(folder, "arrange") - - os.makedirs(self.cache_path, exist_ok=True) - self.preview_btn.setEnabled(False) # 选择新文件夹后禁用预览按钮 - - def get_base_directory(self): - """获取脚本或可执行文件的基础目录""" - if getattr(sys, 'frozen', False): - # 打包后的可执行文件环境 - base_path = os.path.dirname(sys.executable) - else: - # 正常脚本运行环境 - base_path = os.path.dirname(os.path.abspath(__file__)) - return base_path - - def on_run_clicked(self): - # 获取脚本所在目录的父目录 - script_dir = os.path.dirname(os.path.abspath(__file__)) - parent_dir = os.path.dirname(script_dir) - - # 获取基础目录 - base_path = self.get_base_directory() - # 获取父目录 - parent_dir = os.path.dirname(base_path) - bad_dir = os.path.join(parent_dir, "bad") - full_dir = os.path.join(parent_dir, "full") - - # 检查bad目录 - bad_dir_exists = os.path.exists(bad_dir) and os.path.isdir(bad_dir) - bad_dir_not_empty = bad_dir_exists and any(os.scandir(bad_dir)) - - # 检查full目录 - full_dir_exists = os.path.exists(full_dir) and os.path.isdir(full_dir) - full_dir_not_empty = full_dir_exists and any(os.scandir(full_dir)) - - # 如果有异常数据需要处理 - if bad_dir_not_empty or full_dir_not_empty: - message = "请处理以下目录中的异常数据:\n" - if bad_dir_not_empty: - message += f"- bad目录: {bad_dir}\n" - if full_dir_not_empty: - message += f"- full目录: {full_dir}\n" - - QMessageBox.warning(self, "存在未处理的异常数据", - message + "\n请先处理这些目录中的数据后再进行排版!") - self.run_status_label.setText("⚠️ 存在未处理的异常数据,请先处理!") - return - - def threadFunc1(): - self.is_running = True # 标记排版开始 - if not self.folder_path: - self.run_status_label.setText("❗请先选择一个文件夹再执行!") - self.is_running = False - return - - if self.process and self.process.state() == QProcess.Running: - self.run_status_label.setText("⚠️ 正在执行中,请稍候...") - self.is_running = False - return - - self.run_btn.setEnabled(False) - self.preview_btn.setEnabled(False) # 排版中禁用预览按钮 - self.run_status_label.setText( - f" 正在使用 [{self.selected_mode}] 排版, 机型: {self.selected_machine}, 输出格式: {self.output_format}, 请稍候..." - ) - - normalized_path = os.path.normpath(self.folder_path) - self.batch_id = os.path.basename(normalized_path) - - # 调用排版函数,传递所有参数 - self.dw.print_type_setting_obj( - base_original_obj_dir=self.folder_path, - cache_type_setting_dir=self.cache_path, - show_chart=False, - batch_id=self.batch_id, - selected_mode=self.selected_mode, - output_format=self.output_format, - selected_machine=self.selected_machine - ) - - self.run_status_label.setText( - f"✅ [{self.selected_mode}] 排版完成! 机型: {self.selected_machine}, 输出格式: {self.output_format}" - ) - self.run_btn.setEnabled(True) - self.preview_btn.setEnabled(True) # 排版完成后启用预览按钮 - self.is_running = False # 标记排版结束 - - thread = Thread(target=threadFunc1) - thread.start() - - def on_process_finished(self, exitCode, exitStatus): - self.run_btn.setEnabled(True) - self.preview_btn.setEnabled(True) # 完成后启用预览按钮 - if exitCode == 0: - self.run_status_label.setText("✅ 排版完成!") - else: - self.run_status_label.setText(f"❌❌ 进程异常退出 (代码: {exitCode})") - - def on_process_error(self, error): - self.run_btn.setEnabled(True) - self.preview_btn.setEnabled(False) # 出错时禁用预览按钮 - self.run_status_label.setText(f"❌❌ 发生错误: {error.name}") - QMessageBox.critical(self, "错误", f"进程执行出错: {error.name}") - - def open_file_cross_platform(self, path): - if not os.path.exists(path): - self.run_status_label.setText("⚠️ 路径不存在!") - return - if sys.platform.startswith('win'): - os.startfile(path) - elif sys.platform.startswith('darwin'): - QProcess.startDetached("open", [path]) - else: - QProcess.startDetached("xdg-open", [path]) - - def on_preview_clicked(self): - """预览按钮点击事件处理""" - if not self.cache_path: - self.run_status_label.setText("⚠️ 请先执行排版操作!") - return - - is_small_machine = self.selected_machine=="小机型" - - if os.path.exists(self.folder_path): - self.run_status_label.setText("正在打开预览目录...") - self.dw.preview( - base_original_obj_dir=self.folder_path,batch_id=self.batch_id, is_small_machine=is_small_machine - ) - else: - self.run_status_label.setText("⚠️ 预览目录不存在!") - - def on_open_output_clicked(self): - """打开排版结果文件夹""" - if not self.cache_path: - self.run_status_label.setText("⚠️ 请先执行排版操作!") - return - - open_dir = self.cache_path - if self.output_format=="JSON": - open_dir = self.folder_path - - if os.path.exists(open_dir): - self.open_file_cross_platform(open_dir) - else: - self.run_status_label.setText("⚠️ 输出文件夹不存在!") - - -if __name__ == "__main__": - from PyQt5.QtCore import QSharedMemory - app = QApplication(sys.argv) - shared_mem = QSharedMemory("AutoLayoutTool_unique_key") - if not shared_mem.create(1): - QMessageBox.critical(None, "错误", "程序已经在运行中!") - sys.exit(1) - window = AutoLayoutApp() - window.show() - sys.exit(app.exec_()) \ No newline at end of file diff --git a/print_type_setting_gui.spec b/print_type_setting_gui.spec deleted file mode 100644 index 958559e..0000000 --- a/print_type_setting_gui.spec +++ /dev/null @@ -1,38 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -a = Analysis( - ['print_type_setting_gui.py'], - pathex=[], - binaries=[], - datas=[], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - noarchive=False, - optimize=0, -) -pyz = PYZ(a.pure) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.datas, - [], - name='print_type_setting_gui', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) diff --git a/print_type_setting_gui_multi.py b/print_type_setting_gui_multi.py deleted file mode 100644 index abeb5b3..0000000 --- a/print_type_setting_gui_multi.py +++ /dev/null @@ -1,126 +0,0 @@ -import sys -import os -from PyQt5.QtWidgets import (QApplication, QWidget, QPushButton, QLabel, - QVBoxLayout, QFileDialog, QMessageBox) -from PyQt5.QtCore import QProcess, QTimer -from threading import Thread -import print_factory_type_setting_obj_run - - -class AutoLayoutApp(QWidget): - def __init__(self): - super().__init__() - self.setWindowTitle("自动排版工具") - self.setGeometry(200, 200, 600, 200) - self.dw= print_factory_type_setting_obj_run - self.folder_path = "" - self.cache_path = "" - self.process = None - - self.init_ui() - - def init_ui(self): - layout = QVBoxLayout() - - self.folder_path_label = QLabel("�� 请选择要排版的文件夹") - self.folder_path_label.setWordWrap(True) - - self.run_status_label = QLabel("") - - layout.addWidget(self.folder_path_label) - layout.addWidget(self.run_status_label) - - self.select_folder_btn = QPushButton("选择文件夹") - self.select_folder_btn.clicked.connect(self.on_select_folder) - layout.addWidget(self.select_folder_btn) - - self.run_btn = QPushButton("开始自动排版") - self.run_btn.clicked.connect(self.on_run_clicked) - layout.addWidget(self.run_btn) - - self.open_output_btn = QPushButton("打开排版好的文件夹") - self.open_output_btn.clicked.connect(self.on_open_output_clicked) - layout.addWidget(self.open_output_btn) - - self.setLayout(layout) - - def on_select_folder(self): - folder = QFileDialog.getExistingDirectory(self, "选择文件夹") - if folder: - self.folder_path = folder - self.folder_path_label.setText(f"�� 当前选择文件夹: {folder}") - self.run_status_label.setText("") - self.cache_path = folder + "_arrange" - os.makedirs(self.cache_path, exist_ok=True) - - def on_run_clicked(self): - def threadFunc1(): - if not self.folder_path: - self.run_status_label.setText("❗请先选择一个文件夹再执行!") - return - - if self.process and self.process.state() == QProcess.Running: - self.run_status_label.setText("⚠️ 正在执行中,请稍候...") - return - - self.run_btn.setEnabled(False) - self.run_status_label.setText("�� 程序正在运行,请稍候...") - - return_code = self.dw.print_type_setting_obj(base_original_obj_dir=self.folder_path,cache_type_setting_dir=self.cache_path, - show_chart=False) - if return_code==0: - self.run_status_label.setText("✅ 排版完成!") - elif return_code==-1: - self.run_status_label.setText("❌选择目录为空!") - else: - self.run_status_label.setText("❌排版失败!") - self.run_btn.setEnabled(True) - - thread = Thread(target=threadFunc1) - thread.start() - - def on_process_finished(self, exitCode, exitStatus): - self.run_btn.setEnabled(True) - if exitCode == 0: - self.run_status_label.setText("✅ 排版完成!") - else: - self.run_status_label.setText(f"❌ 进程异常退出 (代码: {exitCode})") - - def on_process_error(self, error): - self.run_btn.setEnabled(True) - self.run_status_label.setText(f"❌ 发生错误: {error.name}") - QMessageBox.critical(self, "错误", f"进程执行出错: {error.name}") - - def open_file_cross_platform(self, path): - if not os.path.exists(path): - self.run_status_label.setText("⚠️ 路径不存在!") - return - if sys.platform.startswith('win'): - os.startfile(path) - elif sys.platform.startswith('darwin'): - QProcess.startDetached("open", [path]) - else: - QProcess.startDetached("xdg-open", [path]) - - def on_open_output_clicked(self): - if not self.cache_path: - self.run_status_label.setText("⚠️ 请先执行排版操作!") - return - - output_path = os.path.join(self.cache_path, "print_compact_obj") - if os.path.exists(output_path): - self.open_file_cross_platform(output_path) - else: - self.run_status_label.setText("⚠️ 输出文件夹不存在!") - - -if __name__ == "__main__": - from PyQt5.QtCore import QSharedMemory - app = QApplication(sys.argv) - shared_mem = QSharedMemory("AutoLayoutTool_unique_key") - if not shared_mem.create(1): - QMessageBox.critical(None, "错误", "程序已经在运行中!") - sys.exit(1) - window = AutoLayoutApp() - window.show() - sys.exit(app.exec_()) \ No newline at end of file diff --git a/qt5_demo.py b/qt5_demo.py deleted file mode 100644 index 30ec0e6..0000000 --- a/qt5_demo.py +++ /dev/null @@ -1,25 +0,0 @@ -import sys -from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QMessageBox - -class MyWindow(QWidget): - def __init__(self): - super().__init__() - - self.setWindowTitle("PyQt5 简单示例") - self.setGeometry(100, 100, 300, 200) - - self.button = QPushButton("点我一下", self) - self.button.setGeometry(100, 80, 100, 30) - self.button.clicked.connect(self.show_message) - - def show_message(self): - QMessageBox.information(self, "提示", "按钮被点击了!") - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = MyWindow() - window.show() - sys.exit(app.exec_()) - """ - pyinstaller qt5_demo.py --hidden-import PySide2.QtXml - """ diff --git a/qt5_demo.spec b/qt5_demo.spec deleted file mode 100644 index 85043d8..0000000 --- a/qt5_demo.spec +++ /dev/null @@ -1,44 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -a = Analysis( - ['qt5_demo.py'], - pathex=[], - binaries=[], - datas=[], - hiddenimports=['PySide2.QtXml'], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - noarchive=False, - optimize=0, -) -pyz = PYZ(a.pure) - -exe = EXE( - pyz, - a.scripts, - [], - exclude_binaries=True, - name='qt5_demo', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) -coll = COLLECT( - exe, - a.binaries, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='qt5_demo', -) diff --git a/sui_01.py b/sui_01.py deleted file mode 100644 index 75c0268..0000000 --- a/sui_01.py +++ /dev/null @@ -1,275 +0,0 @@ -import open3d as o3d -import os -import numpy as np -from scipy.spatial.transform import Rotation -import sys - -import argparse -# import cv2 -import matplotlib.pyplot as plt -import numpy as np -from numba import njit, prange -import time - - -# 核心计算函数(支持Numba加速) -@njit(fastmath=True, cache=True) -def calculate_rotation_z(angle_x, angle_y, angle_z, points, cos_cache, sin_cache, angle_step): - """计算单个旋转组合后的重心Z坐标(无显式平移)""" - # 获取预计算的三角函数值 - idx_x = angle_x // angle_step - idx_y = angle_y // angle_step - idx_z = angle_z // angle_step - - cos_x = cos_cache[idx_x] - sin_x = sin_cache[idx_x] - cos_y = cos_cache[idx_y] - sin_y = sin_cache[idx_y] - cos_z = cos_cache[idx_z] - sin_z = sin_cache[idx_z] - - # 构造旋转矩阵(展开矩阵乘法优化) - # R = Rz @ Ry @ Rx - # 计算矩阵元素(手动展开矩阵乘法) - m00 = cos_z * cos_y - m01 = cos_z * sin_y * sin_x - sin_z * cos_x - m02 = cos_z * sin_y * cos_x + sin_z * sin_x - - m10 = sin_z * cos_y - m11 = sin_z * sin_y * sin_x + cos_z * cos_x - m12 = sin_z * sin_y * cos_x - cos_z * sin_x - - m20 = -sin_y - m21 = cos_y * sin_x - m22 = cos_y * cos_x - - # 计算所有点的Z坐标 - z_values = np.empty(points.shape[0], dtype=np.float64) - for i in prange(points.shape[0]): - x, y, z = points[i, 0], points[i, 1], points[i, 2] - # 应用旋转矩阵 - rotated_z = m20 * x + m21 * y + m22 * z - z_values[i] = rotated_z - - # 计算重心Z(等效于平移后的重心) - min_z = np.min(z_values) - avg_z = np.mean(z_values) - return avg_z - min_z # 等效于平移后的重心Z坐标 - - -# 并行优化主函数 -def parallel_rotation2(points, angle_step=3): - """ - 参数: - points : numpy.ndarray (N,3) - 三维点云 - angle_step : int - 角度搜索步长(度数) - - 返回: - (best_angle_x, best_angle_y, best_angle_z, min_z) - """ - points = np.ascontiguousarray(points.astype(np.float64)) - - # 生成所有可能角度 - angles = np.arange(0, 360, angle_step) - n_angles = len(angles) - - # 预计算三角函数值(大幅减少重复计算) - rads = np.radians(angles) - cos_cache = np.cos(rads).astype(np.float64) - sin_cache = np.sin(rads).astype(np.float64) - - # 生成所有角度组合(内存优化版) - total_combinations = n_angles ** 3 - print(f"Total combinations: {total_combinations:,}") - - # 分块处理以避免内存溢出 - best_z = np.inf - best_angles = (0, 0, 0) - batch_size = 10 ** 6 # 根据可用内存调整 - - for x_chunk in range(0, n_angles, max(1, n_angles // 4)): - angles_x = angles[x_chunk:x_chunk + max(1, n_angles // 4)] - for y_chunk in range(0, n_angles, max(1, n_angles // 4)): - angles_y = angles[y_chunk:y_chunk + max(1, n_angles // 4)] - - # 生成当前分块的所有组合 - xx, yy, zz = np.meshgrid(angles_x, angles_y, angles) - current_batch = np.stack([xx.ravel(), yy.ravel(), zz.ravel()], axis=1) - - # 处理子批次 - for i in range(0, len(current_batch), batch_size): - batch = current_batch[i:i + batch_size] - results = np.zeros(len(batch), dtype=np.float64) - _process_batch(batch, points, cos_cache, sin_cache, angle_step, results) - - # 更新最佳结果 - min_idx = np.argmin(results) - if results[min_idx] < best_z: - best_z = results[min_idx] - best_angles = tuple(batch[min_idx]) - print(f"New best: {best_angles} -> Z={best_z:.4f}") - - return (*best_angles, best_z) - - -@njit(parallel=True, fastmath=True) -def _process_batch(batch, points, cos_cache, sin_cache, angle_step, results): - for i in prange(len(batch)): - ax, ay, az = batch[i] - results[i] = calculate_rotation_z( - ax, ay, az, points, - cos_cache, sin_cache, angle_step - ) - - -class ModelProcessor: - def __init__(self): - - # argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] - parser = argparse.ArgumentParser() - - parser.add_argument( - "--id", - required=False, - ) - - args = parser.parse_args() - - self.id = args.id - - self.mesh = None - self.asset_dir = f"/home/algo/Documents/datasets/{self.id}" - - def load_model(self): - """加载并初始化3D模型""" - # model_path = f"{self.asset_dir}/baked/{self.id}.obj" - # model_path = f"{self.asset_dir}/repair_{self.id}_mesh.ply" - model_path = "/data/datasets_20t/8/88884_253283_P65951_6cm_x1.obj" - if not os.path.exists(model_path): - raise FileNotFoundError(f"Model file not found: {model_path}") - - print(model_path) - - mesh_native = o3d.io.read_triangle_mesh(model_path, enable_post_processing=False) - # self.mesh = o3d.io.read_triangle_mesh(model_path, enable_post_processing=False) - - print("Open3D去重前顶点数:", len(mesh_native.vertices)) - self.mesh = mesh_native.merge_close_vertices(eps=1e-6) - - vertices2 = np.asarray(self.mesh.vertices) - print("Open3D去重后顶点数:", len(vertices2)) - vertices2_sorted = sorted( - vertices2.tolist(), - key=lambda x: (x[0], x[1], x[2]) - ) - - if not self.mesh.has_vertex_colors(): - num_vertices = len(self.mesh.vertices) - self.mesh.vertex_colors = o3d.utility.Vector3dVector( - np.ones((num_vertices, 3)) - ) - - self.uv_array = np.asarray(self.mesh.triangle_uvs) - # print(f"UV 坐标形状:{self.uv_array.shape}, {self.uv_array[0][1]}") - - def calculate_rotation_and_center_of_mass(self, angle_x, angle_y, angle_z, points): - """计算某一组旋转角度后的重心""" - # 计算绕X轴、Y轴和Z轴的旋转矩阵 - R_x = np.array([ - [1, 0, 0], - [0, np.cos(np.radians(angle_x)), -np.sin(np.radians(angle_x))], - [0, np.sin(np.radians(angle_x)), np.cos(np.radians(angle_x))] - ]) - - R_y = np.array([ - [np.cos(np.radians(angle_y)), 0, np.sin(np.radians(angle_y))], - [0, 1, 0], - [-np.sin(np.radians(angle_y)), 0, np.cos(np.radians(angle_y))] - ]) - - R_z = np.array([ - [np.cos(np.radians(angle_z)), -np.sin(np.radians(angle_z)), 0], - [np.sin(np.radians(angle_z)), np.cos(np.radians(angle_z)), 0], - [0, 0, 1] - ]) - - # 综合旋转矩阵 - R = R_z @ R_y @ R_x - - # 执行旋转 - rotated_points = points @ R.T - - # 计算最小z值 - min_z = np.min(rotated_points[:, 2]) - - # 计算平移向量,将最小Z值平移到0 - translation_vector = np.array([0, 0, -min_z]) - rotated_points += translation_vector - - # 计算重心 - center_of_mass = np.mean(rotated_points, axis=0) - - return center_of_mass[2], angle_x, angle_y, angle_z - - def parallel_rotation(self, angle_step=3): - """顺序计算最优旋转角度(单线程)""" - min_center_of_mass_y = float('inf') - best_angle_x, best_angle_y, best_angle_z = 0, 0, 0 - - # 遍历所有角度组合 - for angle_x in range(0, 360, angle_step): - for angle_y in range(0, 360, angle_step): - for angle_z in range(0, 360, angle_step): - center_of_mass_z, ax, ay, az = self.calculate_rotation_and_center_of_mass( - angle_x, angle_y, angle_z, self.mesh.vertices - ) - if center_of_mass_z < min_center_of_mass_y: - min_center_of_mass_y = center_of_mass_z - best_angle_x, best_angle_y, best_angle_z = ax, ay, az - - return best_angle_x, best_angle_y, best_angle_z, min_center_of_mass_y - - def process(self): - """执行完整处理流程""" - self.load_model() - - try: - start = time.time() - - # mesh = o3d.geometry.TriangleMesh() - # mesh.vertices = o3d.utility.Vector3dVector(np.random.rand(100, 3)) - # points = np.asarray(mesh.vertices) - - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(self.mesh.vertices) - - # 自动计算合理体素大小 - raw_points = np.asarray(pcd.points) - bounds = np.ptp(raw_points, axis=0) - voxel_size = np.max(bounds) / 50 # 默认取最大边长的2% - - # 执行下采样并验证 - pcd_downsampled = pcd.voxel_down_sample(voxel_size) - if len(pcd_downsampled.points) < 10: # 最少保留10个点 - raise RuntimeError(f"下采样失败:voxel_size={voxel_size:.3f}过大") - - print(f"下采样后点数: {len(pcd_downsampled.points)} (voxel_size={voxel_size:.3f})") - - # pcd.paint_uniform_color([1,0,0]) # 原始红色 - # pcd_downsampled.paint_uniform_color([0,0,1]) # 采样后蓝色 - # o3d.visualization.draw_geometries([pcd, pcd_downsampled]) - - # 继续后续处理 - points = np.asarray(pcd_downsampled.points) - - best_angle_x, best_angle_y, best_angle_z, min_z = parallel_rotation2(points, angle_step=5) - print("best=", best_angle_x, best_angle_y, best_angle_z, min_z) - print(time.time() - start) - - except Exception as e: - print(f"Error during processing: {str(e)}") - raise - - -if __name__ == "__main__": - ModelProcessor().process() \ No newline at end of file diff --git a/test.py b/test.py deleted file mode 100644 index 06a520a..0000000 --- a/test.py +++ /dev/null @@ -1,19 +0,0 @@ -import requests -import json - -printId = 84258 -url = f"https://mp.api.suwa3d.com/api/printOrder/infoByPrintId?printId={printId}" -res = requests.get(url) -print(res) - -datas = res.json()["data"]["layout"] -print(datas) -angle_x = datas.get("angle_x",0) -angle_y = datas.get("angle_y",0) -angle_z = datas.get("angle_z",0) -layout_z = datas.get("layout_z",0) -print("angle_x",angle_x) -print("angle_y",angle_y) -print("angle_z",angle_z) -print("layout_z",layout_z) -# TODO 解析 res diff --git a/test_load_json.py b/test_load_json.py index 13c3c78..bada2d7 100644 --- a/test_load_json.py +++ b/test_load_json.py @@ -5,24 +5,8 @@ import os from PIL import Image import argparse -def custom_mesh_transform(vertices, transform_matrix): - """ - 手动实现网格变换:对每个顶点应用齐次变换矩阵 - 参数: - vertices: 网格顶点数组 (N, 3) - transform_matrix: 4x4 齐次变换矩阵 - 返回: - 变换后的顶点数组 (N, 3) - """ - # 1. 顶点转齐次坐标 (N, 3) → (N, 4) - homogeneous_vertices = np.hstack((vertices, np.ones((vertices.shape[0], 1)))) - - # 2. 应用变换矩阵:矩阵乘法 (4x4) * (4xN) → (4xN) - transformed_homogeneous = transform_matrix @ homogeneous_vertices.T - - # 3. 转回非齐次坐标 (3xN) → (N, 3) - transformed_vertices = transformed_homogeneous[:3, :].T - return transformed_vertices +from config import print_factory_type_dir +from general import mesh_transform_by_matrix def load_and_transform_models(base_path, dict_origin, blank_path, json_name): @@ -46,6 +30,8 @@ def load_and_transform_models(base_path, dict_origin, blank_path, json_name): print(f"读取JSON文件失败: {e}") return [] + print(f"选择机型={data.get('summary')['selected_machine']}") + # 处理每个模型 for model in data.get('models', []): obj_name = model.get('file_name', '') @@ -86,7 +72,7 @@ def load_and_transform_models(base_path, dict_origin, blank_path, json_name): # 手动变换顶点 original_vertices = np.asarray(mesh.vertices) - transformed_vertices = custom_mesh_transform(original_vertices, reconstructed_matrix) + transformed_vertices = mesh_transform_by_matrix(original_vertices, reconstructed_matrix) mesh.vertices = o3d.utility.Vector3dVector(transformed_vertices) # 添加到列表 @@ -467,16 +453,16 @@ def load_show_save(base_path, dict_origin, blank_path, batch_id, is_show=False): if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--batch_id", type=str, required=True, help="batch_id") + parser.add_argument("--batch_id", type=str, required=False, help="batch_id") args = parser.parse_args() # batch_id = args.batch_id - batch_id = "9" # 1113-MY-4 + batch_id = "test" # 1113-MY-4 - print_factory_type_dir="/root/print_factory_type" base_path = f"{print_factory_type_dir}/{batch_id}/" - # blank_path = "{print_factory_type_dir}/blank/blank_bias/blank2.obj" - blank_path = f"{print_factory_type_dir}/blank/blank_bias/blank_small.obj" + blank_path = f"{print_factory_type_dir}/blank/blank_bias/blank2.obj" + # blank_path = f"{print_factory_type_dir}/blank/blank_bias/blank_small.obj" + print(f"blank_path={blank_path}") load_show_save(base_path, {}, blank_path, batch_id, True) \ No newline at end of file diff --git a/x_y_min_test.py b/x_y_min_test.py deleted file mode 100644 index bca4e4d..0000000 --- a/x_y_min_test.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import shutil -import time -import random -import matplotlib.pyplot as plt -import open3d as o3d -import numpy as np - - - -# ply_read_path="/data/datasets_20t/type_setting_test_data/print_bounds_compact_data/88884_253283_P65951_6cm_x1=7.811+11.043+25.699.ply" -# # 读取点云 -# pcd = o3d.io.read_point_cloud(ply_read_path) -# -# # 获取点云的点数据 -# points = np.asarray(pcd.points) -# -# # 计算质心 -# centroid = np.mean(points, axis=0) -# -# # 计算 Y 轴最小值 -# min_y_value = np.min(points[:, 1]) # Y 轴最小值 -# max_y_value = np.max(points[:, 1]) -# -# # 计算 X 轴最小值 -# min_x_value = np.min(points[:, 0]) # X 轴最小值 -# -# print(f'min_x_value{min_x_value}') -# min_x_value -385.08287729332403 -# -ply_read_path="/data/datasets_20t/type_setting_test_data/print_bounds_compact_data/456450_260316_P65976_2.66cm_x1=21.778+22.904+26.333.ply" -# 读取点云 -pcd = o3d.io.read_point_cloud(ply_read_path) - -# 获取点云的点数据 -points = np.asarray(pcd.points) - -# 计算质心 -centroid = np.mean(points, axis=0) - -# 计算 Y 轴最小值 -min_y_value = np.min(points[:, 1]) # Y 轴最小值 -max_y_value = np.max(points[:, 1]) - -# 计算 X 轴最小值 -min_x_value = np.min(points[:, 0]) # X 轴最小值 - -print(f'min_x_value{min_x_value}') -# min_x_value -385.08287729332403 -print(f'min_y_value{min_y_value}') - -# -339 \ No newline at end of file diff --git a/读写时间测试.py b/读写时间测试.py deleted file mode 100644 index 54e0813..0000000 --- a/读写时间测试.py +++ /dev/null @@ -1,152 +0,0 @@ -# import numpy as np -# def calculate_rotation_and_center_of_mass(angle_x, angle_y, angle_z, points): -# """计算某一组旋转角度后的重心""" -# # 计算绕X轴、Y轴和Z轴的旋转矩阵 -# R_x = np.array([ -# [1, 0, 0], -# [0, np.cos(np.radians(angle_x)), -np.sin(np.radians(angle_x))], -# [0, np.sin(np.radians(angle_x)), np.cos(np.radians(angle_x))] -# ]) -# -# R_y = np.array([ -# [np.cos(np.radians(angle_y)), 0, np.sin(np.radians(angle_y))], -# [0, 1, 0], -# [-np.sin(np.radians(angle_y)), 0, np.cos(np.radians(angle_y))] -# ]) -# -# R_z = np.array([ -# [np.cos(np.radians(angle_z)), -np.sin(np.radians(angle_z)), 0], -# [np.sin(np.radians(angle_z)), np.cos(np.radians(angle_z)), 0], -# [0, 0, 1] -# ]) -# -# # 综合旋转矩阵 -# R = R_z @ R_y @ R_x -# -# # 执行旋转 -# rotated_points = points @ R.T -# -# # 计算最小z值 -# min_z = np.min(rotated_points[:, 2]) -# -# # 计算平移向量,将最小Z值平移到0 -# translation_vector = np.array([0, 0, -min_z]) -# rotated_points += translation_vector -# -# # 计算重心 -# center_of_mass = np.mean(rotated_points, axis=0) -# -# return center_of_mass[2], angle_x, angle_y, angle_z -# -# def parallel_rotation(points, angle_step=3): -# """顺序计算最优旋转角度(单线程)""" -# min_center_of_mass_y = float('inf') -# best_angle_x, best_angle_y, best_angle_z = 0, 0, 0 -# -# # 遍历所有角度组合 -# for angle_x in range(0, 360, angle_step): -# for angle_y in range(0, 360, angle_step): -# for angle_z in range(0, 360, angle_step): -# center_of_mass_z, ax, ay, az = calculate_rotation_and_center_of_mass( -# angle_x, angle_y, angle_z, points -# ) -# if center_of_mass_z < min_center_of_mass_y: -# min_center_of_mass_y = center_of_mass_z -# best_angle_x, best_angle_y, best_angle_z = ax, ay, az -# -# return best_angle_x, best_angle_y, best_angle_z, min_center_of_mass_y - - -import bpy -import time -import os -from pathlib import Path - - -def clear_scene(): - """清空当前场景中的所有对象""" - bpy.ops.object.select_all(action='SELECT') - bpy.ops.object.delete() - - -def test_bpy_io(obj_path, output_path): - """测试 bpy 的 OBJ 读写性能""" - # 读取 OBJ - start_time = time.time() - bpy.ops.import_scene.obj(filepath=obj_path) - read_time = time.time() - start_time - - # 确保场景中有对象 - if not bpy.context.scene.objects: - raise ValueError("未成功导入 OBJ 文件") - - # 写入 OBJ - start_time = time.time() - bpy.ops.export_scene.obj( - filepath=output_path, - use_selection=False, # 导出所有对象 - use_materials=False, # 不导出材质(加快速度) - ) - write_time = time.time() - start_time - - # 清理场景 - clear_scene() - - return write_time, read_time - - -def test_folder_objs_with_bpy(folder_path, output_folder="output_objs_bpy"): - """测试文件夹中所有 OBJ 文件的读写性能(使用 bpy)""" - # 确保输出文件夹存在 - Path(output_folder).mkdir(exist_ok=True) - - # 收集所有 OBJ 文件 - obj_files = [f for f in os.listdir(folder_path) if f.lower().endswith('.obj')] - - if not obj_files: - print(f"在文件夹 {folder_path} 中未找到 OBJ 文件") - return - - print(f"找到 {len(obj_files)} 个 OBJ 文件,开始测试...") - - results = [] - - for obj_file in obj_files: - input_path = os.path.join(folder_path, obj_file) - output_path = os.path.join(output_folder, f"bpy_{obj_file}") - - print(f"\n测试文件: {obj_file}") - - try: - write_time, read_time = test_bpy_io(input_path, output_path) - file_size = os.path.getsize(input_path) / (1024 * 1024) # MB - - print(f" 文件大小: {file_size:.2f} MB") - print(f" bpy 读取时间: {read_time:.3f}s") - print(f" bpy 写入时间: {write_time:.3f}s") - - results.append({ - "filename": obj_file, - "size_mb": file_size, - "read_time": read_time, - "write_time": write_time, - }) - - except Exception as e: - print(f" 处理 {obj_file} 时出错: {e}") - - # 计算平均时间 - if results: - avg_read = sum(r["read_time"] for r in results) / len(results) - avg_write = sum(r["write_time"] for r in results) / len(results) - print("\n=== 汇总结果 ===") - print(f"平均读取时间: {avg_read:.3f}s") - print(f"平均写入时间: {avg_write:.3f}s") - - -if __name__ == "__main__": - # 设置包含OBJ文件的文件夹路径 - obj_folder = "/data/datasets_20t/9_big/" # 替换为你的OBJ文件夹路径 - - # 运行测试 - test_folder_objs_with_bpy(obj_folder) diff --git a/读写时间测试2.py b/读写时间测试2.py deleted file mode 100644 index 25bc71a..0000000 --- a/读写时间测试2.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import time -import open3d as o3d -import trimesh -from pathlib import Path - - -def test_folder_objs(folder_path, output_folder="output_objs"): - """测试文件夹中所有OBJ文件的读写性能""" - # 确保输出文件夹存在 - Path(output_folder).mkdir(exist_ok=True) - - # 收集文件夹中所有OBJ文件 - obj_files = [f for f in os.listdir(folder_path) if f.lower().endswith('.obj')] - - if not obj_files: - print(f"在文件夹 {folder_path} 中未找到OBJ文件") - return - - print(f"找到 {len(obj_files)} 个OBJ文件,开始测试...") - - # 准备结果记录 - results = [] - - for obj_file in obj_files: - file_path = os.path.join(folder_path, obj_file) - output_path = os.path.join(output_folder, obj_file) - - print(f"\n测试文件: {obj_file}") - - # 测试open3d - o3d_write, o3d_read = test_open3d_io(file_path, output_path.replace('.obj', '_o3d.obj')) - - # 测试trimesh - tm_write, tm_read = test_trimesh_io(file_path, output_path.replace('.obj', '_tm.obj')) - - # 记录结果 - file_stats = { - 'filename': obj_file, - 'o3d_write': o3d_write, - 'o3d_read': o3d_read, - 'tm_write': tm_write, - 'tm_read': tm_read, - 'write_ratio': o3d_write / tm_write if tm_write > 0 else 0, - 'read_ratio': o3d_read / tm_read if tm_read > 0 else 0 - } - results.append(file_stats) - - # 打印当前文件结果 - print(f" open3d | 写入: {o3d_write:.3f}s | 读取: {o3d_read:.3f}s") - print(f" trimesh | 写入: {tm_write:.3f}s | 读取: {tm_read:.3f}s") - print(f" 写入速度比(trimesh/open3d): {file_stats['write_ratio']:.1f}x") - print(f" 读取速度比(trimesh/open3d): {file_stats['read_ratio']:.1f}x") - - # 打印汇总结果 - print("\n=== 汇总结果 ===") - avg_write_ratio = sum(r['write_ratio'] for r in results) / len(results) - avg_read_ratio = sum(r['read_ratio'] for r in results) / len(results) - print(f"平均写入速度比(trimesh/open3d): {avg_write_ratio:.1f}x") - print(f"平均读取速度比(trimesh/open3d): {avg_read_ratio:.1f}x") - - -def test_open3d_io(input_path, output_path): - """测试open3d的读写性能""" - # 读取 - start = time.time() - mesh = o3d.io.read_triangle_mesh(input_path) - read_time = time.time() - start - - # 写入 - start = time.time() - o3d.io.write_triangle_mesh(output_path, mesh) - write_time = time.time() - start - - return write_time, read_time - - -def test_trimesh_io(input_path, output_path): - """测试trimesh的读写性能""" - # 读取 - start = time.time() - mesh = trimesh.load(input_path) - read_time = time.time() - start - - # 写入 - start = time.time() - mesh.export(output_path) - write_time = time.time() - start - - return write_time, read_time - - -if __name__ == "__main__": - # 设置包含OBJ文件的文件夹路径 - obj_folder = "/data/datasets_20t/9_big/" # 替换为你的OBJ文件夹路径 - - # 运行测试 - test_folder_objs(obj_folder) \ No newline at end of file