diff --git a/.bash_history b/.bash_history old mode 100644 new mode 100755 index 164ea44..ac329b3 --- a/.bash_history +++ b/.bash_history @@ -1,500 +1,500 @@ -./2-infer.py /nn/7091925/20230208/CT/9 /nn/7091925/20230208/MR/7 -./1-move.py 6035638 -./2-infer.py /nn/6035638/20230209/CT/8 /nn/6035638/20230209/MR/7 -./1-move.py 7030265 -./2-infer.py /nn/7030265/20230209/CT/9 /nn/7030265/20230209/MR/7 -./1-move.py 6981439 -./2-infer.py /nn/6981439/20230210/CT/a /nn/6981439/20230210/MR/7 -./1-move.py 5665595 -./2-infer.py /nn/5665595/20230210/CT/9 /nn/5665595/20230210/MR/7 -./1-move.py 3903066 -./2-infer.py /nn/3903066/20230213/CT/a /nn/3903066/20230213/MR/7 -./1-move.py 3903066 -./2-infer.py /nn/3903066/20230213/CT/9 /nn/3903066/20230213/MR/7 -./1-move.py 5487071 -./2-infer.py /nn/5487071/20230213/CT/9 /nn/5487071/20230213/MR/7 -./1-move.py 6771041 -./2-infer.py /nn/6771041/20230214/CT/9 /nn/6771041/20230214/MR/7 -./1-move.py 5495897 -./2-infer.py /nn/5495897/20230214/CT/9 /nn/5495897/20230214/MR/7 -./1-move.py 6987123 -./2-infer.py /nn/6987123/20230215/CT/9 /nn/6987123/20230215/MR/7 -./1-move.py 3183636 -./2-infer.py /nn/3183636/20230215/CT/9 /nn/3183636/20230215/MR/7 -./1-move.py 5768342 -./2-infer.py /nn/5768342/20230216/CT/9 /nn/5768342/20230216/MR/7 -./1-move.py 5695691 -./2-infer.py /nn/5695691/20230216/CT/a /nn/5695691/20230216/MR/7 -./1-move.py 5768342 -./2-infer.py /nn/5768342/20230216/CT/9 /nn/5768342/20230216/MR/7 -./1-move.py 7123316 -./2-infer.py /nn/7123316/20230220/CT/9 /nn/7123316/20230220/MR/7 -./1-move.py 5831210 -./2-infer.py /nn/5831210/20230220/CT/c /nn/5831210/20230220/MR/a -./1-move.py 3387187 -./2-infer.py /nn/3387187/20230221/CT/a /nn/3387187/20230221/MR/7 -./1-move.py 6803836 -./2-infer.py /nn/6803836/20230221/CT/10 /nn/6803836/20230221/MR/e -./1-move.py -./2-infer.py /nn/6466293/20230222/CT/a /nn/6466293/20230222/MR/7 -./1-move.py 2669302 -./2-infer.py /nn/2669302/20230223/CT/a /nn/2669302/20230223/MR/7 -./1-move.py 4607357 -./2-infer.py /nn/4607357/20230224/CT/a /nn/4607357/20230224/MR/7 -./1-move.py 7055212 -./2-infer.py /nn/7055212/20230224/CT/9 /nn/7055212/20230224/MR/7 -./1-move.py 5027429 -./2-infer.py /nn/5027429/20230306/CT/b /nn/5027429/20230306/MR/9 -./1-move.py 7123530 -./2-infer.py /nn/7123530/20230307/CT/a /nn/7123530/20230307/MR/7 -./1-move.py 7130932 -./2-infer.py /nn/7130932/20230307/CT/a /nn/7130932/20230307/MR/7 -./1-move.py 5771396 -./2-infer.py /nn/5771396/20230308/CT/a /nn/5771396/20230308/MR/7 -./1-move.py 7128131 -./2-infer.py /nn/7128131/20230310/CT/a /nn/7128131/20230310/MR/7 -./1-move.py 6412311 -./2-infer.py /nn/6412311/20230313/CT/9 /nn/6412311/20230313/MR/7 -./1-move.py -./2-infer.py /nn/5494530/20230313/CT/a /nn/5494530/20230313/MR/7 -./1-move.py 7128706 -./2-infer.py /nn/7128706/20230310/CT/c /nn/7128706/20230310/MR -./1-move.py 6925186 -./2-infer.py /nn/6925186/20230314/CT/a /nn/6925186/20230314/MR/7 -./1-move.py 3863530 -./1-move.py 3863530 -./2-infer.py /nn/3863530/20230314/CT/a /nn/3863530/20230314/MR/7 -./1-move.py 7131041 -./2-infer.py /nn/7131041/20230315/CT/a /nn/7131041/20230315/MR/7 -./1-move.py 5682089 -./2-infer.py /nn/5682089/20230315/CT/a /nn/5682089/20230315/MR/7 -./1-move.py 5553220 -./2-infer.py /nn/5553220/20230316/CT/a /nn/5553220/20230316/MR/7 -./1-move.py 3589548 -./2-infer.py /nn/3589548/20230317/CT/9 /nn/3589548/20230317/MR/7 -./1-move.py 2111161 -./2-infer.py /nn/2111161/20230317/CT/9 /nn/2111161/20230317/MR/7 -./1-move.py 6167673 -./2-infer.py /nn/6167673/20230320/CT/9 /nn/6167673/20230320/MR/7 -./1-move.py 2372501 -./2-infer.py /nn/2372501/20230320/CT/c /nn/2372501/20230320/MR/2 -./1-move.py 7134146 -./2-infer.py /nn/7134146/20230321/CT/a /nn/7134146/20230321/MR/7 -./1-move.py 6017134 -./2-infer.py /nn/6017134/20230321/CT/9 /nn/6017134/20230321/MR/7 -./1-move.py 5061967 -./2-infer.py /nn/5061967/20230322/CT/b /nn/5061967/20230322/MR/8 -./1-move.py 3060758 -./2-infer.py /nn/3060758/20230322/CT/a /nn/3060758/20230322/MR/7 -./1-move.py 6943475 -./2-infer.py /nn/6943475/20230323/CT/9 /nn/6943475/20230323/MR/7 -./1-move.py 5027838 -./2-infer.py /nn/5027838/20230323/CT/a /nn/5027838/20230323/MR/7 -./1-move.py 6783185 -./2-infer.py /nn/6783185/20230324/CT/a /nn/6783185/20230324/MR/7 -./1-move.py 4482158 -./2-infer.py /nn/4482158/20230324/CT/9 /nn/4482158/20230324/MR/7 -ls /nn -ls -ltr /nn -ls -./1-move.py 4482158 -./1-move.py 6764185 -./2-infer.py /nn/6764185/20230327/CT/9 /nn/6764185/20230327/MR/7 -./1-move.py -./2-infer.py /nn/7127857/20230327/CT/a /nn/7127857/20230327/MR/7 -./1-move.py 6553099 -./2-infer.py /nn/6553099/20230328/CT/9 /nn/6553099/20230328/MR/7 -./1-move.py 7040640 -./2-infer.py /nn/7040640/20230328/CT/a /nn/7040640/20230328/MR/7 -./1-move.py 6553099 -./2-infer.py /nn/6553099/20230328/CT/9 /nn/6553099/20230328/MR/7 -./1-move.py 7106897 -./2-infer.py /nn/7106897/20230329/CT/a /nn/7106897/20230329/MR/7 -./1-move.py 5261092 -./2-infer.py /nn/5261092/20230329/CT/a /nn/5261092/20230329/MR/7 -./1-move.py 4301162 -./2-infer.py /nn/4301162/20230330/CT/d /nn/4301162/20230330/MR/7 -./1-move.py 7131077 +python3 oar.py /nn/7208298/20241213/CT/a /nn/7208298/20241213/MR/7 +python3 oar.py /nn/7208298/20241213/CT/a /nn/7208298/20241213/MR/7 +export nnUNet_results=/123/onlylian/nnUNet_results +export nnUNet_preprocessed=/123/onlylian/nnUNet_preprocessed +export nnUNet_results=/123/onlylian/nnUNet_results +cd /123/onlylian/nnUNet_results +ls -l +ls -l Dataset505 +python3 oar.py /nn/7208298/20241213/CT/a /nn/7208298/20241213/MR/7 +cd .. +python3 oar.py /nn/7208298/20241213/CT/a /nn/7208298/20241213/MR/7 +python3 oar.py /nn/7208298/20241213/CT/a /nn/7208298/20241213/MR/7 +export nnUNet_raw="/onlylian/nnUNet_raw" +export nnUNet_preprocessed="/onlylian/nnUNet_preprocessed" +export nnUNet_results="/onlylian/nnUNet_results" +echo $nnUNet_raw +echo $nnUNet_preprocessed +echo $nnUNet_results +nnUNetv2_predict -i /nn/7208298/20241213/input -o /nn/7208298/20241213/output -d 505 -c 3d_fullres -f 0 -tr nnUNetTrainer -p nnUNetPlans --checkpoint /onlylian/nnUNet_results/Dataset505/nnUNetTrainer__nnUNetPlans__3d_fullres/fold_0/checkpoint_final.pth +nnUNetv2_predict -i /nn/7208298/20241213/input -o /nn/7208298/20241213/output -d 505 -c 3d_fullres -f 0 -tr nnUNetTrainer -p nnUNetPlans +ls -l /onlylian/nnUNet_results +ls -l /onlylian/nnUNet_raw +python3 oar.py /nn/7208298/20241213/CT/a /nn/7208298/20241213/MR/7 +/nn/7208298/20241213/output +ls -l /nn/7208298/20241213/output +cat /nn/7208298/20241213/output/[filename] +cp -r /nn/7208298/20241213/output ./onlylian +cp -r /nn/7208298/20241213/output ./onlylian +cp -r /nn/7208298/20241213/input ./onlylian -./1-move.py 6996427 -./1-move.py 6996427 -./2-infer.py /nn/6996427/20230331/CT/a /nn/6996427/20230331/MR/7 +python3 oar.py /nn/7208298/20241213/CT/a /nn/7208298/20241213/MR/7 +cp -r /nn/7208298/20241213/output ./onlylian +python3 oar.py /nn/7208298/20241213/CT/a /nn/7208298/20241213/MR/7 +cp -r /nn/7208298/20241213/output ./onlylian +python3 oar.py /nn/2783853/20241217/CT/9 /nn/2783853/20241217/MR/8 +python3 oar.py /nn/7208298/20241213/CT/a /nn/7208298/20241213/MR/7 +cp -r /nn/7208298/20241213/output ./onlylian +python3 oar.py /nn/2783853/20241217/CT/9 /nn/2783853/20241217/MR/ +python3 oar.py /nn/4608443/20241219/CT/a /nn/4608443/20241219/MR/7 +python3 oar.py /nn/7208298/20241213/CT/a /nn/7208298/20241213/MR/7 +cp -r /nn/7208298/20241213/output ./onlylian +python3 oar.py /nn/5108920/20241223/CT/a /nn/5108920/20241223/MR/7 +python3 oar.py /nn/5435462/20241224/CT/9 /nn/5435462/20241224/MR/7 +python3 oar.py /nn/2944822/20241224/CT/e /nn/2944822/20241224/MR/9 +python3 oar.py /nn/7287366/20241225/CT/9 /nn/7287366/20241225/MR/7 +python3 oar.py /nn/6405610/20241225/CT/9 /nn/6405610/20241225/MR/7 +python3 oar.py /nn/7286032/20241226/CT/b /nn/7286032/20241226/MR/9 +python3 oar.py /nn/1786330/20241226/CT/b /nn/1786330/20241226/MR/8 +python3 oar.py /nn/7284108/20241227/CT/b /nn/7284108/20241227/MR/8 +python3 oar.py /nn/4365592/20241227/CT/a /nn/4365592/20241227/MR/7 +python3 oar.py /nn/2367245/20241223/CT/b /nn/2367245/20241231/MR/7 +python3 oar.py /nn/3018505/20250102/CT/9 /nn/3018505/20250102/MR/8 +python3 oar.py /nn/6827885/20250102/CT/a /nn/6827885/20250102/MR/7 +python3 oar.py /nn/2556090/20250103/CT/a /nn/2556090/20250103/MR/7 +python3 oar.py /nn/6902522/20250103/CT/a /nn/6902522/20250103/MR/7 +python3 oar.py /nn/5908183/20250106/CT/a /nn/5908183/20250106/MR/7 +python3 oar.py /nn/7165972/20250106/CT/a /nn/7165972/20250106/MR/7 +python3 oar.py /nn/6248872/20250108/CT/a /nn/6248872/20250108/MR/8 +python3 oar.py /nn/7223127/20250108/CT/a /nn/7223127/20250108/MR/6 +python3 oar.py /nn/4398001/20250114/CT/9 /nn/4398001/20250114/MR/7 +python3 oar.py /nn/4003636/20250114/CT/9 /nn/4003636/20250114/MR/8 +python3 oar.py /nn/5131302/20250116/CT/9 /nn/5131302/20250116/MR/7 +python3 oar.py /nn/5337151/20250116/CT/9 /nn/5337151/20250103/MR/b +python3 oar.py /nn/7283904/20250116/CT/9 /nn/7283904/20250116/MR/7 +python3 oar.py /nn/5517865/20250117/CT/a /nn/5517865/20250117/MR/8 +python3 oar.py /nn/7231788/20250120/CT/a /nn/7231788/20250120/MR/8 +python3 oar.py /nn/3427537/20250120/CT/a /nn/3427537/20250120/MR/8 +python3 oar.py /nn/7154553/20250122/CT/9 /nn/7154553/20250122/MR/7 +python3 oar.py /nn/7239152/20250124/CT/a /nn/7239152/20250124/MR/7 +python3 oar.py /nn/2957658/20250124/CT/b /nn/2957658/20250124/MR/8 +python3 oar.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +conda env list +ls -l +python oar.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +./1-move.py 4608443 ./1-move.py 1561614 -./2-infer.py /nn/1561614/20230331/CT/b /nn/1561614/20230331/MR/8 -top -exit -./1-move.py -./2-infer.py /nn/6623737/20230406/CT/8 /nn/6623737/20230406/MR/7 -./1-move.py 6631848 -./2-infer.py /nn/6631848/20220810/CT/9 /nn/6631848/20230406/MR/7 -./1-move.py 6631848 -./2-infer.py /nn/6631848/20230406/CT/a /nn/6631848/20230406/MR/7 -./2-infer.py /nn/6631848/20230406/CT/a /nn/6631848/20230406/MR/7 -./2-infer.py /nn/6631848/20230406/CT/a /nn/6631848/20230406/MR/7 -./2-infer.py /nn/6631848/20230406/CT/a /nn/6631848/20230406/MR/7 -./2-infer.py /nn/6631848/20230406/CT/a /nn/6631848/20230406/MR/7 -./1-move.py 6418004 -./1-move.py 8045956 -./2-infer.py /nn/8045956/20230407/CT/b /nn/8045956/20230407/MR/7 -./1-move.py 6418004 -./2-infer.py /nn/6418004/20230407/CT/8 /nn/6418004/20230407/MR/7 -./1-move.py 2644799 -./2-infer.py /nn/2644799/20230410/CT/a /nn/2644799/20230410/MR/7 -./1-move.py 4379301 -./2-infer.py /nn/4379301/20230410/CT/a /nn/4379301/20230410/MR/7 -./1-move.py 6563617 -./2-infer.py /nn/6563617/20230411/CT/9 /nn/6563617/20230411/MR/2 -./1-move.py 6739334 -./2-infer.py /nn/6739334/20230411/CT/8 /nn/6739334/20230411/MR/7 -./1-move.py 6393784 -./2-infer.py /nn/6393784/20230412/CT/9 /nn/6393784/20230412/MR/7 -./1-move.py 6962333 -./2-infer.py /nn/6962333/20230412/CT/9 /nn/6962333/20230412/MR/7 -./1-move.py 6474814 -./2-infer.py /nn/6474814/20230413/CT/a /nn/6474814/20230413/MR/7 -./1-move.py 2430604 -./2-infer.py /nn/2430604/20230413/CT/a /nn/2430604/20230413/MR/7 -./1-move.py 4572736 -./2-infer.py /nn/4572736/20230414/CT/a /nn/4572736/20230414/MR/7 -./1-move.py 3905496 -./2-infer.py /nn/3905496/20230414/CT/a /nn/3905496/20230414/MR/7 -./1-move.py 5767800 -./2-infer.py /nn/5767800/20230417/CT/a /nn/5767800/20230417/MR/7 -./1-move.py 6789767 -./2-infer.py /nn/6789767/20230417/CT/9 /nn/6789767/20230417/MR/7 -./1-move.py 5760888 -./2-infer.py /nn/5760888/20230418/CT/8 /nn/5760888/20230418/MR/7 -./1-move.py 6700596 -/nn/6700596/20230418/CT/9 /nn/6700596/20230418/MR/7 -./2-infer.py /nn/6700596/20230418/CT/9 /nn/6700596/20230418/MR/7 -./1-move.py 2334044 -./2-infer.py /nn/2334044/20230419/CT/9 /nn/2334044/20230419/MR/7 -./1-move.py 3001393 -./1-move.py 3001393 -./2-infer.py /nn/3001393/20230419/CT/a /nn/3001393/20230419/MR/7 -./1-move.py 6596248 -./2-infer.py /nn/6596248/20230420/CT/a /nn/6596248/20230420/MR/6 -./1-move.py 7104717 -./2-infer.py /nn/7104717/20230420/CT/a /nn/7104717/20230420/MR/6 -./1-move.py 7132783 -./2-infer.py /nn/7132783/20230421/CT/8 /nn/7132783/20230421/MR/7 -./1-move.py 7096197 -./2-infer.py /nn/7096197/20230424/CT/a /nn/7096197/20230424/MR/7 -./1-move.py 6841107 -./2-infer.py /nn/6841107/20230424/CT/9 /nn/6841107/20230424/MR/7 -./1-move.py 2739367 -./2-infer.py /nn/2739367/20230425/CT/8 /nn/2739367/20230425/MR/7 -./1-move.py 5789401 -./2-infer.py /nn/5789401/20230425/CT/a /nn/5789401/20230425/MR/7 -./1-move.py 1845653 -./2-infer.py /nn/1845653/20230426/CT/a /nn/1845653/20230426/MR/7 -./1-move.py 5956729 -./2-infer.py /nn/5956729/20230426/CT/a /nn/5956729/20230426/MR/7 -./1-move.py 5956729 -./2-infer.py /nn/5956729/20230426/CT/a /nn/5956729/20230426/MR/7 -./1-move.py 7137162 -./2-infer.py /nn/7137162/20230427/CT/a /nn/7137162/20230427/MR/7 -./1-move.py 8003599 -./2-infer.py /nn/8003599/20230427/CT/a /nn/8003599/20230427/MR/7 -./1-move.py 7138225 -./2-infer.py /nn/7138225/20230428/CT/a /nn/7138225/20230428/MR/7 -./1-move.py 3698229 -./2-infer.py /nn/3698229/20230428/CT/a /nn/3698229/20230428/MR/7 -./1-move.py 3391186 -./2-infer.py /nn/3391186/20230501/CT/9 /nn/3391186/20230501/MR/7 -./1-move.py 3698229 -./2-infer.py /nn/3698229/20230428/CT/9 /nn/3698229/20230428/MR/6 -./1-move.py 6793968 -./2-infer.py /nn/6793968/20230501/CT/d /nn/6793968/20230501/MR/7 -./1-move.py 6815738 -./2-infer.py /nn/6815738/20230502/CT/8 /nn/6815738/20230502/MR/7 -./1-move.py 7108421 -./2-infer.p -./1-move.py 4255849 -./2-infer.py /nn/4255849/20230503/CT/9 /nn/4255849/20230503/MR/6 -./1-move.py 6688931 -./2-infer.py /nn/6688931/20230503/CT/a /nn/6688931/20230503/MR/7 -./1-move.py 6688931 -./2-infer.py /nn/6688931/20230503/CT/a /nn/6688931/20230503/MR/7 -./1-move.py 6719072 -./2-infer.py /nn/6719072/20230504/CT/a /nn/6719072/20230504/MR/7 -./1-move.py 6805343 -./2-infer.py /nn/6805343/20230504/CT/a /nn/6805343/20230504/MR/1 -./1-move.py 4273476 -./2-infer.py /nn/4273476/20230505/CT/a /nn/4273476/20230505/MR/7 -./1-move.py 7104717 -./2-infer.py /nn/7104717/20230505/CT/a /nn/7104717/20230505/MR/7 -./1-move.py 5689608 -./2-infer.py /nn/5689608/20230508/CT/a /nn/5689608/20230508/MR/7 -./1-move.py 3277833 -./2-infer.py /nn/3277833/20230508/CT/a /nn/3277833/20230508/MR/7 -./1-move.py 3463014 -./1-move.py 2772372 -./2-infer.py /nn/2772372/20230509/CT/a /nn/2772372/20230509/MR/7 -./1-move.py 5726776 -./2-infer.py /nn/5726776/20230510/CT/5 /nn/5726776/20230510/MR/2 -./1-move.py 3276632 -./2-infer.py /nn/3276632/20230511/CT/9 /nn/3276632/20230511/MR/7 -./1-move.py 3566418 -./2-infer.py /nn/3566418/20230511/CT/9 /nn/3566418/20230511/MR/7 -./1-move.py 3566418 -/nn/3566418/20230511/CT/8 /nn/3566418/20230511/MR/7 -./2-infer.py /nn/3566418/20230511/CT/8 /nn/3566418/20230511/MR/7 -./1-move.py 3994172 -./2-infer.py /nn/3994172/20230512/CT/8 /nn/3994172/20230512/MR/7 -./1-move.py 5868361 -./2-infer.py /nn/5868361/20230512/CT/8 /nn/5868361/20230512/MR/7 -./1-move.py 7146459 -./2-infer.py /nn/7146459/20230515/CT/a /nn/7146459/20230515/MR/7 -./1-move.py 7124386 -./1-move.py 7124386 -./2-infer.py /nn/7124386/20230515/CT/a /nn/7124386/20230515/MR/7 -./1-move.py 7127462 -./2-infer.py /nn/7127462/20230516/CT/4 /nn/7127462/20230516/MR/3 -./1-move.py 6510305 -./2-infer.py /nn/6510305/20230516/CT/9 /nn/6510305/20230516/MR/7 -./1-move.py 7128841 -./2-infer.py /nn/7128841/20230517/CT/8 /nn/7128841/20230517/MR/6 -./1-move.py 6400837 -./2-infer.py /nn/6400837/20230517/CT/8 /nn/6400837/20230517/MR/7 -./1-move.py 6743139 -./2-infer.py /nn/6743139/20230518/CT/c /nn/6743139/20230518/MR/6 -./1-move.py 3561770 -./2-infer.py /nn/3561770/20230518/CT/9 /nn/3561770/20230518/MR/6 -./1-move.py 3561770 -./2-infer.py /nn/3561770/20230518/CT/9 /nn/3561770/20230518/MR/7 -./1-move.py 7138891 -./2-infer.py /nn/7138891/20230522/CT/9 MAR /nn/7138891/20230522/MR/7 -./1-move.py 2508411 -./2-infer.py /nn/2508411/20230522/CT/a /nn/2508411/20230522/MR/7 -./1-move.py 7138891 -/nn/7138891/20230522/CT/9 /nn/7138891/20230522/MR/7 -./2-infer.py /nn/7138891/20230522/CT/9 /nn/7138891/20230522/MR/7 -./1-move.py 7021220 -./2-infer.py /nn/7021220/20230523/CT/9 /nn/7021220/20230523/MR/7 -./1-move.py 3978624 -./2-infer.py /nn/3978624/20230523/CT/9 /nn/3978624/20230523/MR/7 -./1-move.py 7144666 -./2-infer.py /nn/7144666/20230525/CT/9 /nn/7144666/20230525/MR/7 -./1-move.py 6861756 -./2-infer.py /nn/6861756/20230525/CT/a /nn/6861756/20230525/MR/7 -./1-move.py 4500037 -./2-infer.py /nn/4500037/20230526/CT/a /nn/4500037/20230526/MR/6 -./1-move.py 7128016 -./2-infer.py /nn/7128016/20230526/CT/a /nn/7128016/20230526/MR/6 -./1-move.py 7135342 -./2-infer.py /nn/7135342/20230529/CT/b /nn/7135342/20230529/MR/8 -./1-move.py 7046657 -./2-infer.py /nn/7046657/20230529/CT/a /nn/7046657/20230529/MR/7 -./1-move.py 5494513 -./2-infer.py /nn/5494513/20230530/CT/5 /nn/5494513/20230530/MR/2 -./1-move.py -./2-infer -./1-move.py 6825963 -./2-infer.py /nn/6825963/20230531/CT/9 /nn/6825963/20230531/MR/7 -./1-move.py 7074124 -./2-infer.py /nn/7074124/20230531/CT/b /nn/7074124/20230531/MR/7 -./1-move.py 5168451 -./2-infer.py /nn/5168451/20230601/CT/9 /nn/5168451/20230601/MR/7 -./1-move.py -./2-infer.py /nn/3924866/20230601/CT/a /nn/3924866/20230601/MR/7 -./1-move.py 7008354 -./2-infer.py /nn/7008354/20230602/CT/8 /nn/7008354/20230602/MR/7 -./1-move.py 5665595 -./2-infer.py /nn/5665595/20230605/CT/9 /nn/5665595/20230605/MR/7 -./1-move.py 7115427 -./2-infer.py /nn/7115427/20230605/CT/9 /nn/7115427/20230605/MR/7 -./1-move.py 2567501 -./2-infer.py /nn/2567501/20230606/CT/8 /nn/2567501/20230606/MR/7 -./1-move.py 5569945 -./2-infer.p -./1-move.py 7144930 -./2-infer.py /nn/7144930/20230607/CT/9 /nn/7144930/20230607/MR/7 -./1-move.py 6744457 -./2-infer.py /nn/6744457/20230607/CT/9 /nn/6744457/20230607/MR/7 -./1-move.py 2313478 -./2-infer.py /nn/2313478/20230608/CT/a /nn/2313478/20230608/MR/6 +./2-infer.py /nn/1561614/20241220/CT/a /nn/1561614/20241220/MR/8 +./1-move.py 2641998 +./2-infer.py /nn/2641998/20241220/CT/c /nn/2641998/20241220/MR/3 +./1-move.py 5108920 +./1-move.py 5435462 +./1-move.py 2944822 +./1-move.py 7287366 +./1-move.py 6405610 +./1-move.py 7286032 +./1-move.py 1786330 +./1-move.py 7284108 +./1-move.py 7365592 +./1-move.py 4365592 +./1-move.py 6901803 +./2-infer.py /nn/6901803/20241230/CT/9 /nn/6901803/20241230/MR/8 ./1-move.py 7146030 -./2-infer.py /nn/7146030/20230609/CT/a /nn/7146030/20230609/MR/7 -./1-move.py 1994656 -./2-infer.py /nn/1994656/20230609/CT/a /nn/1994656/20230609/MR/7 -./1-move.py 1784858 -./2-infer.py /nn/1784858/20230612/CT/a /nn/1784858/20230612/MR/7 -./1-move.py 6507299 -./2-infer.py /nn/6507299/20230612/CT/a /nn/6507299/20230612/MR/7 -./1-move.py 6305556 -./2-infer.py /nn/6305556/20230613/CT/9 /nn/6305556/[A -./1-move.py 6305556 -./2-infer.py /nn/6305556/20230613/CT/9 /nn/6305556/20230613/MR/7 -./1-move.py -./2-infer.py /nn/2559243/20230613/CT/a /nn/2559243/20230613/MR/7 -./1-move.py 7112382 -./2-infer.py /nn/7112382/20230614/CT/a /nn/7112382/20230614/MR/7 -./1-move.py 6460301 -./2-infer.py /nn/6460301/20230614/CT/9 /nn/6460301/20230614/MR/7 -nvidia-smi -./1-move.py 1325527 -./2-infer.py /nn/1325527/20230615/CT/9 /nn/1325527/20230615/MR/7 -./1-move.py 7018112 -./2-infer.py /nn/7018112/20230615/CT/9 /nn/7018112/20230615/MR/7 -./1-move.py 6647069 -./2-infer.py /nn/6647069/20230616/CT/a /nn/6647069/20230616/MR/7 -./1-move.py 7150687 -./2-infer.py /nn/7150687/20230616/CT/9 /nn/7150687/20230616/MR/7 -./1-move.py 6746593 -./2-infer.py /nn/6746593/20230619/CT/9 /nn/6746593/20230619/MR/7 -./1-move.py 7024631 -./2-infer.py /nn/7024631/20230619/CT/a /nn/7024631/20230619/MR/7 -./1-move.py 3384498 -./2-infer.py /nn/3384498/20230620/CT/a /nn/3384498/20230620/MR/7 -./1-move.py 7113684 -./2-infer.py /nn/7113684/20230620/CT/a /nn/7113684/20230620/MR/7 -./1-move.py 5053997 -./2-infer.py /nn/5053997/20230621/CT/a /nn/5053997/20230621/MR/7 -./1-move.py 5522920 -./2-infer.py /nn/5522920/20230621/CT/a /nn/5522920/20230621/MR/7 -./1-move.py 5522920 -./2-infer.py /nn/5522920/20230621/CT/a /nn/5522920/20230621/MR/7 -./1-move.py 7152084 -./2-infer.py /nn/7152084/20230623/CT/a /nn/7152084/20230623/MR/7 -./1-move.py 6354781 -./2-infer.py /nn/6354781/20230623/CT/a /nn/6354781/20230623/MR/7 -./1-move.py 3950552 -./2-infer.py /nn/3950552/20230626/CT/8 /nn/3950552/20230626/MR/7 -./1-move.py 6699766 -./2-infer.py /nn/6699766/20230626/CT/9 /nn/6699766/20230626/MR/7 -./1-move.py 7143289 -./2-infer.py /nn/7143289/20230627/CT/8 /nn/7143289/20230627/MR/7 -./1-move.py 6716839 -./2-infer.py /nn/6716839/20230627/CT/8 /nn/6716839/20230627/MR/7 -./1-move.py 7152103 -./2-infer.py /nn/7152103/20230628/CT/9 /nn/7152103/20230628/MR/7 -./1-move.py 6119992 -./2-infer.py /nn/6119992/20230628/CT/9 /nn/6119992/20230628/MR/7 -./1-move.py 6224878 -./2-infer.py /nn/6224878/20230629/CT/9 /nn/6224878/20230629/MR/6 -./1-move.py 7147425 -./2-infer.py /nn/7147425/20230629/CT/9 /nn/7147425/20230629/MR/7 -./1-move.py 7152233 -./2-infer.py /nn/7152233/20230630/CT/a /nn/7152233/20230630/MR/6 -./1-move.py 711176 -./2-infer.py /nn/7111769/20230630/CT/a /nn/7111769/20230630/MR/7 -./1-move.py 7130901 -./2-infer.py /nn/7130901/20230703/CT/a /nn/7130901/20230703/MR/7 -./1-move.py 7150422 +./2-infer.py /nn/7146030/20241230/CT/b /nn/7146030/20241230/MR/8 +./1-move.py 3560986 +./2-infer.py /nn/3560986/20241231/CT/9 /nn/3560986/20241231/MR/8 +./1-move.py 6075924 +./2-infer.py /nn/6075924/20241231/CT/a /nn/6075924/20241231/MR/8 +./1-move.py 2367245 +./2-infer.py /nn/2367245/20241223/CT/b /nn/2367245/20241231/MR/7 +./1-move.py 2367245 +./1-move.py 3018505 +./1-move.py 6827885 +./1-move.py 2556090 +./1-move.py 6902522 +./1-move.py 5908183 +./1-move.py 7165972 +./1-move.py 5835974 +./2-infer.py /nn/5835974/20250107/CT/9 /nn/5835974/20250107/MR/7 +./1-move.py 7239182 -./1-move.py 7111769 -./1-move.py 6106176 -./2-infer.py /nn/6106176/20230704/CT/a /nn/6106176/20230704/MR/7 -./1-move.py 3889924 -./2-infer.py /nn/3889924/20230704/CT/9 /nn/3889924/20230704/MR/7 -./1-move.py 3678428 -./2-infer.py /nn/3678428/20230705/CT/a /nn/3678428/20230705/MR/6 -./1-move.py 7155718 -./2-infer.py /nn/7155718/20230705/CT/a /nn/7155718/20230705/MR/7 -./1-move.py -./1-move.py 6803603 -./2-infer.py /nn/6803603/20230706/CT/9 /nn/6803603/20230706/MR/7 -./1-move.py 4153961 -./2-infer.py /nn/4153961/20230706/CT/9 /nn/4153961/20230706/MR/7 -./1-move.py -./2-infer.py /nn/6510305/20230707/CT/8 /nn/6510305/20230707/MR/7 -./1-move.py 5369780 -./2-infer.py /nn/5369780/20230707/CT/9 /nn/5369780/20230707/MR/7 -./1-move.py 7111644 -./2-infer.py /nn/7111644/20230710/CT/9 /nn/7111644/20230710/MR/7 -./1-move.py 3247320 -./2-infer.py /nn/3247320/20230710/CT/9 /nn/3247320/20230710/MR/7 -./1-move.py 3782801 -./1-move.py 3782810 -./2-infer.py /nn/3782810/20230711/CT/a /nn/3782810/20230711/MR/7 -./1-move.py 7047930 -./2-infer.py /nn/7047930/20230711/CT/8 /nn/7047930/20230711/MR/2 -./1-move.py 7155947 -./2-infer.py /nn/7155947/20230712/CT/9 /nn/7155947/20230712/MR/7 -./1-move.py 6607568 -./2-infer.py /nn/6607568/20230712/CT/9 /nn/6607568/20230712/MR/7 -./1-move.py 5638465 -./2-infer.py /nn/5638465/20230713/CT/9 /nn/5638465/20230713/MR/2 -./1-move.py 2855422 -./2-infer.py /nn/2855422/20230713/CT/9 /nn/2855422/20230713/MR/2 -./1-move.py 4118618 -./2-infer.py /nn/4118618/20230714/CT/d /nn/4118618/20230714/MR/8 -./1-move.py 7138156 -./2-infer.py /nn/7138156/20230714/CT/9 /nn/7138156/20230714/MR/7 -22444455555555444444./1-move.py 14444454555 -./1-move.py 4214975 -./2-infer.py /nn/4214975/20230717/CT/c /nn/4214975/20230717/MR/8 -./1-move.py 3485443 -./2-infer.py /nn/3485443/20230717/CT/a /nn/3485443/20230717/MR/7 -./1-move.py 6824186 -./2-infer.py /nn/6824186/20230718/CT/9 /nn/6824186/20230718/MR/7 -./1-move.py 6547497 -./2-infer.py /nn/6547497/20230718/CT/8 /nn/6547497/20230718/MR/7 -./1-move.py 1875185 -./2-infer.py /nn/1875185/20230719/CT/a /nn/1875185/20230719/MR/6 -./1-move.py 7023789 -./2-infer.py /nn/7023789/20230719/CT/a /nn/7023789/20230719/MR/6 -./1-move.py 2098952 -./2-infer.py /nn/2098952/20230720/CT/a /nn/2098952/20230720/MR/6 -./1-move.py 7151783 -./2-infer.py /nn/7151783/20230720/CT/a /nn/7151783/20230720/MR/7 -./1-move.py 7075650 -./2-infer.py /nn/7075650/20230721/CT/9 /nn/7075650/20230721/MR/7 -./1-move.py 4352680 -./2-infer.py /nn/4352680/20230721/CT/9 MAR -./2-infer.py /nn/4352680/20230721/CT/9 MAR /nn/4352680/20230721/MR/7 -./1-move.py 4352680 -./2-infer.py /nn/4352680/20230721/CT/a /nn/4352680/20230721/MR/7 -./1-move.py 7144639 -./2-infer.py /nn/7144639/20230724/CT/a /nn/7144639/20230724/MR/3 -./1-move.py 4505405 -./2-infer.py /nn/4505405/20230724/CT/9 /nn/4505405/20230724/MR/7 -./1-move.py 7157433 -./2-infer.py /nn/7157433/20230725/CT/b /nn/7157433/20230725/MR/7 -./1-move.py 3283427 -./2-infer.py /nn/3283427/20230725/CT/9 /nn/3283427/20230725/MR/7 -./1-move.py 6401745 -./2-infer.py /nn/6401745/20230726/CT/a /nn/6401745/20230726/MR/6 -./1-move.py 4383470 -./2-infer.py /nn/4383470 -./1-move.py 1994335 -./2-infer.py /nn/1994335/20230727/CT/9 /nn/1994335/20230727/MR/7 -./1-move.py 6184547 -./2-infer.py /nn/6184547/20230728/CT/a /nn/6184547/20230728/MR/7 -./1-move.py 5223617 -./2-infer.py /nn/5223617/20220520/CT/8 /nn/5223617/20230728/MR/7 -./1-move.py /nn/5223617/20230728/MR/7 - -./1-move.py 5223617 -./1-move.py /nn/5223617/20230728/CT/8 /nn/5223617/20230728/MR/7 -./1-move.py /nn/5223617/20230728/CT/8 /nn/5223617/20230728/MR/7 -./2-infer.py /nn/5223617/20220520/CT/8 /nn/5223617/20230728/MR/7 -./1-move.py 1003532 -./2-infer.py /nn/1003532/20230728/CT/9 /nn/1003532/20230728/MR/2 -./1-move.py 5223617 -./2-infer.py /nn/5223617/20230728/CT/8 /nn/5223617/20230728/MR/7 -./1-move.py 3307080 -./2-infer.py /nn/3307080/20230731/CT/8 /nn/3307080/20230731/MR/7 -./1-move.py 3307080 -./2-infer.py /nn/3307080/20230731/CT/8 /nn/3307080/20230731/MR/7 -./1-move.py 2431392 -./2-infer.py /nn/2431392/20230731/CT/9 /nn/2431392/20230731/MR/2 -./1-move.py 7155758 -./2-infer.py /nn/7155758/20230801/CT/a /nn/7155758/20230801/MR/7 -./1-move.py 5879999 -./2-infer.py /nn/5879999/20230801/CT/a /nn/5879999/20230801/MR/7 -./1-move.py 6460676 -./2-infer.py /nn/6460676/20230802/CT/9 /nn/6460676/20230802/MR/7 -./1-move.py 4107788 -./2-infer.py /nn/4107788/20230802/CT/a /nn/4107788/20230802/MR/7 -./1-move.py 7159471 -./2-infer.py /nn/7159471/20 -./1-move.py 7159471 -./2-infer.py /nn/7159471/20230804/CT/a /nn/7159471/20230804/MR/7 -ps aux -ps aux -ps aux +./1-move.py 7239182 +./2-infer.py /nn/7239182/20250107/CT/8 /nn/7239182/20250107/MR/7 +./1-move.py 6248872 +./2-infer.py /nn/6248872/20250108/CT/a /nn/6248872/20250108/MR/8 +./1-move.py 7223127 +./1-move.py 1918501 +./2-infer.py /nn/1918501/20250109/CT/a /nn/1918501/20250109/MR/8 +./1-move.py 8049454 +./2-infer.py /nn/8049454/20250109/CT/9 /nn/8049454/20250109/MR/8 +./1-move.py A105575 +./1-move.py 6217784 +./2-infer.py /nn/6217784/20250110/CT/2 /nn/A105575/20250107/MR/8 +./1-move.py 6736489 +./2-infer.py /nn/6736489/20250110/CT/a /nn/6736489/20250110/MR/8 +./1-move.py 6052644 +./2-infer.py /nn/6052644/20250110/CT/b /nn/6052644/20250110/MR/9 +./1-move.py 6052644 +./2-infer.py /nn/6052644/20250110/CT/b /nn/6052644/20250110/MR/9 +./1-move.py 2516940 +./2-infer.py /nn/2516940/20250113/CT/a /nn/2516940/20250113/MR/7 +./1-move.py 7190177 +./2-infer.py /nn/7190177/20250113/CT/b /nn/7190177/20250113/MR/8 +./1-move.py 4398001 +./1-move.py 4003636 +./1-move.py 7233916 +./2-infer.py /nn/7233916/20250115/CT/b /nn/7233916/20250115/MR/7 +./1-move.py 7295438 +./2-infer.py /nn/7295438/20250115/CT/d /nn/7295438/20250115/MR/9 +./1-move.py 5337151 +./1-move.py 5337151 +./1-move.py 5337151 +./1-move.py 5131302 +./1-move.py 5337151 +./1-move.py 7283904 +./1-move.py 4182983 +./2-infer.py /nn/4182983/20250117/CT/a /nn/4182983/20250117/MR/6 +./1-move.py 5517865 +./1-move.py 5517865 +./1-move.py 7283904 +./1-move.py 7231788 +./1-move.py 3427537 +./1-move.py 6449647 +./2-infer.py /nn/6449647/20250121/CT/8 /nn/6449647/20250121/MR/7 +./1-move.py 2508411 +./2-infer.py /nn/2508411/20250121/CT/c /nn/2508411/20250121/MR/9 +./1-move.py 7154553 +./1-move.py 7253850 +./2-infer.py /nn/7253850/20250123/CT/b /nn/7253850/20250123/MR/8 +./1-move.py 7240437 +./2-infer.py /nn/7240437/20250123/CT/a /nn/7240437/20250123/MR/8 +./1-move.py 7239152 +./1-move.py 2957658 +./1-move.py 7295866 +./2-infer.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 nvidia-smi nvidia-smi nvidia-smi +cd onlylian/ +ls +python3 oar.py /nn/7295866/20250127/CT/a /nn/72/nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +ls /nn +python3 oar.py /nn/7295866/20250127/CT/a /nn/72/nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +copy oar.py oar2.py +cp oar.py oar2.py +dir /nn/7295866/ +dir /nn/7295866/20250127/ +dir /nn/7295866/20250127/nii/ +dir /nn/7295866/20250127/output/ +dir /nn/7295866/20250127/nii/ +dir /nn/7295866/20250127/input/ +dir /nn/7295866/20250127/output/ +dir /nn/7295866/20250127/CT/ +dir /nn/7295866/20250127/ +ls -lR /nn/7295866/20250127/ +cd onlylian/ +python test2.py +python test2.py +python test2.py +ls /nn/7295866/20250127/output/processed_7_3D_SAG_T1_MPRAGE_+C_20250127132612.nii.gz +ls /nn/7295866/20250127/output/processed_7_3D_SAG_T1_MPRAGE_+C_20250127132612.nii.gz +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +ls /nn/7295866/20250127/ +ls /nn/7295866/20250127/pyradise/ +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +python test2.py +apt install libxrender1 +python test2.py +python test2.py +python test2.py +ls /nn/7295866/20250127/ +ls /nn/7295866/20250127/CT/ +python test2.py +ls /nn/7295866/20250127/CT/ +ls -l /nn/7295866/20250127/CT/ +ls +ls -ltr +copy oar.py oar2.py +cp oar.py oar2.py +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +ls /nn/7295866/20250127/CT/ +ls -ltr /nn/7295866/20250127/CT/ +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +ls -ltr /nn/7295866/20250127/CT/ +ls -ltr /nn/7295866/20250127/CT/a-rtss* +rm -ltr /nn/7295866/20250127/CT/a-rtss* +rm /nn/7295866/20250127/CT/a-rtss* +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +rm -ltr /nn/7295866/20250127/CT/a-rtss* +rm -ltr /nn/7295866/20250127/CT/ +ls /nn/7295866/20250127/CT/ +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +ls /nn/7295866/20250127/CT/ +ls /nn/7295866/20250127/CT/7295866/ +ls -l /nn/7295866/20250127/CT/7295866/ +ls -l /nn/7295866/20250127/pyradise/ +ls -l /nn/7295866/20250127/pyradise/7295866/ +rm -rf /nn/7295866/20250127/pyradise/ +rm -rf /nn/7295866/20250127/pyradise +rm -rf /nn/7295866/20250127/CT/7295866 +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +ls /nn/7295866/20250127/pyradise +ls /nn/7295866/20250127/pyradise +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +/nn/7295866/20250127/pyradise +ls /nn/7295866/20250127/pyradise +ls /nn/7295866/20250127/ +ls /nn/7295866/20250127/seg +rm -rf /nn/7295866/20250127/seg +ls /nn/7295866/20250127/ +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +ls /nn/7295866/20250127/CT/a-rtss.dcm +ls /nn/7295866/20250127/pyradise/ +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +cp oar2.py oar3.py +cd registration +ls +python best_reg.py +cd .. +python registration/best_reg.py +python -m registration.best_reg +python -m registration.best_reg +python -m registration.best_reg +python -m registration.best_reg +python -m registration.best_reg +cd onlylian/ +ls +python -m registration.best_reg +python -m registration.best_reg +python -m registration.best_reg +ls '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz' +ls '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447.label.nii.gz' +python -m registration.best_reg +python -m registration.best_reg +python -m registration.best_reg +ls /nn/7295866/20250127/pyradise +rm -rf /nn/7295866/20250127/pyradise +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +ls nnUNet_results/Dataset222_OAR_TV/ +ls nnUNet_results/Dataset222_OAR_TV/nnUNetTrainer__nnUNetPlans__2d/ +ls nnUNet_results/Dataset222_OAR_TV/nnUNetTrainer__nnUNetPlans__2d/fold_0/ +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +ls /nn/7295866/20250127/pyradise +rm -rf /nn/7295866/20250127/pyradise +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar2.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +chmod 777 /nn/7295866/20250127/ +chmod 777 /nn/7295866/20250127/pyradise/ +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +ls /nn/7295866/20250127/CT/a-rtss.dcm +ping 172.16.40.36 +nnUNetv2_find_best_configuration +export nnUNet_raw"/123/onlylian/nnUNet_raw" +export nnUNet_raw="/123/onlylian/nnUNet_raw" +export nnUNet_results="/123/onlylian/nnUNet_results" +nnUNetv2_find_best_configuration 222 +export nnUNet_preprocessed="/123/onlylian/nnUNet_preprocessed" +nnUNetv2_find_best_configuration 222 +python3 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar3.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +conda install conda-forge::niftyreg +apt search niftyreg +pip install fireants +ps +ps aux +kill 8051 +git clone https://github.com/rohitrango/fireants +rm -rf fireants/ +pip install git+https://github.com/rohitrango/fireants.git +git clone https://github.com/rohitrango/fireants +cd fireants/ +ls +nano pyproject.toml +pip install -e . +pip --version +pip install --upgrade pip +pip install -e . +ㄎcd +cd +cd onlylian/ +ls +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +nvidia-smi +nvidia-smi +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +python -m registration.fireants_reg +ls +ls ~/0/ +ls ~/0/mri_synthmorph.zip +unzip ~/0/mri_synthmorph.zip +ls +cd mri_synthmorph/ +ls +conda +conda list +ls +./mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz moving.nii.gz clipped.nii.gz -g +chmod +x mri_synthmorph +./mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz moving.nii.gz clipped.nii.gz -g +python +pip install git+https://github.com/adalca/neurite.git git+https://github.com/freesurfer/surfa.git git+https://github.com/voxelmorph/voxelmorph.git +./mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz moving.nii.gz clipped.nii.gz -g +conda list +conda install tensorflow-gpu +conda update conda +conda update -n base conda +conda install tensorflow-gpu +conda install -n base conda-libmamba-solver +conda config --set solver libmamba +conda install -n base conda-libmamba-solver +pip install --upgrade pip +conda update -n base conda +c +pip install tensorflow-gpu +./mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz moving.nii.gz clipped.nii.gz -g +ls +cd .. +ls +./mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz '/nn/7295866/20250127/output/processed_7_3D_SAG_T1_MPRAGE_+C_20250127132612.nii.gz'moving.nii.gz clipped.nii.gz -g +./mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz '/nn/7295866/20250127/output/processed_7_3D_SAG_T1_MPRAGE_+C_20250127132612.nii.gz'moving.nii.gz clipped.nii.gz +clear +cd onlylian/ +ls +mri_synthmorph/mri_synthmorph '/nn/7295866/20250127/output/processed_7_3D_SAG_T1_MPRAGE_+C_20250127132612.nii.gz' -m affine -t trans.lta -o out-aff.nii.gz moving.nii.gz clipped.nii.gz -g +mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447_5.nii.gz' '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz' -g +ls +cd mri_synthmorph/ +ls +mkdir models +cd models/ +wget https://surfer.nmr.mgh.harvard.edu/docs/synthmorph/synthmorph.affine.2.h5 +cd .. +cd .. +FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447_5.nii.gz' '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz' -g +ls -ltr +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447_5.nii.gz' '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz' -g +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447_5.nii.gz' '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz' -g +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447_5.nii.gz' '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz' -g +watch nvidia-smi +cp oar3.py oar4.py +watch nvidia-smi +ls +ls *gz +cd onlylian/ +ls +ls *gz +cp '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447_5.nii.gz' ct.nii.gz +cp '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz' t1c.nii.gz +mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz t1c.nii.gz ct.nii.gz -g +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz t1c.nii.gz ct.nii.gz -g +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz t1c.nii.gz ct.nii.gz -g +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz t1c.nii.gz ct.nii.gz -g +python test2.py +ls -ltr +mv *.nii.gz 0/ +mkdir 0 +mv *.nii.gz 0/ + t ime FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t 0/trans.lta -o 0/out-aff.nii.gz 0/t1c.nii.gz 0/clipped.nii.gz -g +rm trans.lta +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t 0/trans.lta -o 0/out-aff.nii.gz 0/t1c.nii.gz 0/clipped.nii.gz -g +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph o 0/out.nii.gz 0/t1c.nii.gz 0/clipped.nii.gz -g +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -o 0/out.nii.gz 0/t1c.nii.gz 0/clipped.nii.gz -g +cd mri_synthmorph/models/ +wget https://surfer.nmr.mgh.harvard.edu/docs/synthmorph/synthmorph.deform.3.h5 +rm synthmorph.deform.3.h5 +cd .. +cd .. +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t 0/trans.lta -o 0/out-aff.nii.gz 0/t1c.nii.gz 0/clipped.nii.gz -g +grep oar3 ~/.bash_history +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +pip install blosc2 +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +pip freeze +pip freeze|grep acvl +pip install acvl-utils==0.2 +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +pip freeze|grep -i nnu +pip install -U nnunetv2 +pip install nnunetv2==2.5.1 +pip install nnunetv2==2.4 +cd onlylian/ +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +conda +apt install libxrender1 +nvcc +nvidia-smi +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +pip install tensorflow[and-cuda] +python3 -c "import tensorflow as tf; print(tf.config.list_physical_devices('GPU'))" +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +ls mri_synthmorph/mri_synthmorph +mri_synthmorph/mri_synthmorph +mri_synthmorph/mri_synthmorph -h +mri_synthmorph/mri_synthmorph register -h +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +python +nvidia-smi +cd onlylian/ +mri_synthmorph/mri_synthmorph register -h +cd onlylian/ +python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 +pip freeze|grep torch diff --git a/.bash_history-00353.tmp b/.bash_history-00353.tmp deleted file mode 100644 index 60638f7..0000000 --- a/.bash_history-00353.tmp +++ /dev/null @@ -1,98 +0,0 @@ -./2-infer.py /nn/7091925/20230208/CT/9 /nn/7091925/20230208/MR/7 -./1-move.py 6035638 -./2-infer.py /nn/6035638/20230209/CT/8 /nn/6035638/20230209/MR/7 -./1-move.py 7030265 -./2-infer.py /nn/7030265/20230209/CT/9 /nn/7030265/20230209/MR/7 -./1-move.py 6981439 -./2-infer.py /nn/6981439/20230210/CT/a /nn/6981439/20230210/MR/7 -./1-move.py 5665595 -./2-infer.py /nn/5665595/20230210/CT/9 /nn/5665595/20230210/MR/7 -./1-move.py 3903066 -./2-infer.py /nn/3903066/20230213/CT/a /nn/3903066/20230213/MR/7 -./1-move.py 3903066 -./2-infer.py /nn/3903066/20230213/CT/9 /nn/3903066/20230213/MR/7 -./1-move.py 5487071 -./2-infer.py /nn/5487071/20230213/CT/9 /nn/5487071/20230213/MR/7 -./1-move.py 6771041 -./2-infer.py /nn/6771041/20230214/CT/9 /nn/6771041/20230214/MR/7 -./1-move.py 5495897 -./2-infer.py /nn/5495897/20230214/CT/9 /nn/5495897/20230214/MR/7 -./1-move.py 6987123 -./2-infer.py /nn/6987123/20230215/CT/9 /nn/6987123/20230215/MR/7 -./1-move.py 3183636 -./2-infer.py /nn/3183636/20230215/CT/9 /nn/3183636/20230215/MR/7 -./1-move.py 5768342 -./2-infer.py /nn/5768342/20230216/CT/9 /nn/5768342/20230216/MR/7 -./1-move.py 5695691 -./2-infer.py /nn/5695691/20230216/CT/a /nn/5695691/20230216/MR/7 -./1-move.py 5768342 -./2-infer.py /nn/5768342/20230216/CT/9 /nn/5768342/20230216/MR/7 -./1-move.py 7123316 -./2-infer.py /nn/7123316/20230220/CT/9 /nn/7123316/20230220/MR/7 -./1-move.py 5831210 -./2-infer.py /nn/5831210/20230220/CT/c /nn/5831210/20230220/MR/a -./1-move.py 3387187 -./2-infer.py /nn/3387187/20230221/CT/a /nn/3387187/20230221/MR/7 -./1-move.py 6803836 -./2-infer.py /nn/6803836/20230221/CT/10 /nn/6803836/20230221/MR/e -./1-move.py -./2-infer.py /nn/6466293/20230222/CT/a /nn/6466293/20230222/MR/7 -./1-move.py 2669302 -./2-infer.py /nn/2669302/20230223/CT/a /nn/2669302/20230223/MR/7 -./1-move.py 4607357 -./2-infer.py /nn/4607357/20230224/CT/a /nn/4607357/20230224/MR/7 -./1-move.py 7055212 -./2-infer.py /nn/7055212/20230224/CT/9 /nn/7055212/20230224/MR/7 -./1-move.py 5027429 -./2-infer.py /nn/5027429/20230306/CT/b /nn/5027429/20230306/MR/9 -./1-move.py 7123530 -./2-infer.py /nn/7123530/20230307/CT/a /nn/7123530/20230307/MR/7 -./1-move.py 7130932 -./2-infer.py /nn/7130932/20230307/CT/a /nn/7130932/20230307/MR/7 -./1-move.py 5771396 -./2-infer.py /nn/5771396/20230308/CT/a /nn/5771396/20230308/MR/7 -./1-move.py 7128131 -./2-infer.py /nn/7128131/20230310/CT/a /nn/7128131/20230310/MR/7 -./1-move.py 6412311 -./2-infer.py /nn/6412311/20230313/CT/9 /nn/6412311/20230313/MR/7 -./1-move.py -./2-infer.py /nn/5494530/20230313/CT/a /nn/5494530/20230313/MR/7 -./1-move.py 7128706 -./2-infer.py /nn/7128706/20230310/CT/c /nn/7128706/20230310/MR -./1-move.py 6925186 -./2-infer.py /nn/6925186/20230314/CT/a /nn/6925186/20230314/MR/7 -./1-move.py 3863530 -./1-move.py 3863530 -./2-infer.py /nn/3863530/20230314/CT/a /nn/3863530/20230314/MR/7 -./1-move.py 7131041 -./2-infer.py /nn/7131041/20230315/CT/a /nn/7131041/20230315/MR/7 -./1-move.py 5682089 -./2-infer.py /nn/5682089/20230315/CT/a /nn/5682089/20230315/MR/7 -./1-move.py 5553220 -./2-infer.py /nn/5553220/20230316/CT/a /nn/5553220/20230316/MR/7 -./1-move.py 3589548 -./2-infer.py /nn/3589548/20230317/CT/9 /nn/3589548/20230317/MR/7 -./1-move.py 2111161 -./2-infer.py /nn/2111161/20230317/CT/9 /nn/2111161/20230317/MR/7 -./1-move.py 6167673 -./2-infer.py /nn/6167673/20230320/CT/9 /nn/6167673/20230320/MR/7 -./1-move.py 2372501 -./2-infer.py /nn/2372501/20230320/CT/c /nn/2372501/20230320/MR/2 -./1-move.py 7134146 -./2-infer.py /nn/7134146/20230321/CT/a /nn/7134146/20230321/MR/7 -./1-move.py 6017134 -./2-infer.py /nn/6017134/20230321/CT/9 /nn/6017134/20230321/MR/7 -./1-move.py 5061967 -./2-infer.py /nn/5061967/20230322/CT/b /nn/5061967/20230322/MR/8 -./1-move.py 3060758 -./2-infer.py /nn/3060758/20230322/CT/a /nn/3060758/20230322/MR/7 -./1-move.py 6943475 -./2-infer.py /nn/6943475/20230323/CT/9 /nn/6943475/20230323/MR/7 -./1-move.py 5027838 -./2-infer.py /nn/5027838/20230323/CT/a /nn/5027838/20230323/MR/7 -./1-move.py 6783185 -./2-infer.py /nn/6783185/20230324/CT/a /nn/6783185/20230324/MR/7 -./1-move.py 4482158 -./2-infer.py /nn/4482158/20230324/CT/9 /nn/4482158/20230324/MR/7 -ls /nn -ls -ltr /nn diff --git a/.conda/environments.txt b/.conda/environments.txt new file mode 100755 index 0000000..71a25d1 --- /dev/null +++ b/.conda/environments.txt @@ -0,0 +1 @@ +/opt/conda diff --git a/.gitconfig b/.gitconfig new file mode 100755 index 0000000..2ef8d0e --- /dev/null +++ b/.gitconfig @@ -0,0 +1,3 @@ +[user] + email = xfr@dr.com + name = xfr diff --git a/.gitignore b/.gitignore index ac95524..a646467 100755 --- a/.gitignore +++ b/.gitignore @@ -160,8 +160,14 @@ cython_debug/ #.idea/ .jupyter/ +.local/ +.npm/ +nnUNet_results/ *.nii *.nii.gz *.mat *.h5 +*.npz +*.pth +*log*.txt diff --git a/.jupyter/jupyter_notebook_config.json b/.jupyter/jupyter_notebook_config.json old mode 100644 new mode 100755 diff --git a/.jupyter/lab/user-settings/@jupyterlab/docmanager-extension/plugin.jupyterlab-settings b/.jupyter/lab/user-settings/@jupyterlab/docmanager-extension/plugin.jupyterlab-settings old mode 100644 new mode 100755 diff --git a/.jupyter/lab/workspaces/lab-a511.jupyterlab-workspace b/.jupyter/lab/workspaces/lab-a511.jupyterlab-workspace old mode 100644 new mode 100755 index ac31f6e..724e353 --- a/.jupyter/lab/workspaces/lab-a511.jupyterlab-workspace +++ b/.jupyter/lab/workspaces/lab-a511.jupyterlab-workspace @@ -1 +1 @@ -{"data":{"layout-restorer:data":{"main":{"dock":{"type":"tab-area","currentIndex":1,"widgets":["editor:1-move.py","terminal:1","terminal:2","editor:2-infer.py","editor:adding.py"]},"mode":"multiple-document","current":"terminal:1"},"left":{"collapsed":false,"current":"filebrowser","widgets":["filebrowser","running-sessions","command-palette","jp-property-inspector","tab-manager","extensionmanager.main-view"]},"right":{"collapsed":true,"widgets":[]}},"@jupyterlab/settingeditor-extension:plugin":{"sizes":[0.16252955082742318,0.8374704491725768],"container":{"plugin":"@jupyterlab/terminal-extension:plugin","sizes":[0.4873891008009604,0.5126108991990396]}},"file-browser-filebrowser:cwd":{"path":""},"editor:2-infer.py":{"data":{"path":"2-infer.py","factory":"Editor"}},"editor:1-move.py":{"data":{"path":"1-move.py","factory":"Editor"}},"editor:adding.py":{"data":{"path":"adding.py","factory":"Editor"}},"terminal:1":{"data":{"name":"1"}},"terminal:2":{"data":{"name":"2"}}},"metadata":{"id":"/lab"}} \ No newline at end of file +{"data":{"layout-restorer:data":{"main":{"dock":{"type":"tab-area","currentIndex":0,"widgets":["editor:docker/v2/Dockerfile","terminal:1","editor:onlylian/oar4.py"]},"mode":"multiple-document","current":"editor:docker/v2/Dockerfile"},"left":{"collapsed":false,"current":"filebrowser","widgets":["filebrowser","running-sessions","command-palette","jp-property-inspector","tab-manager","extensionmanager.main-view"]},"right":{"collapsed":true,"widgets":[]}},"@jupyterlab/settingeditor-extension:plugin":{"sizes":[0.16252955082742318,0.8374704491725768],"container":{"plugin":"@jupyterlab/terminal-extension:plugin","sizes":[0.4873891008009604,0.5126108991990396]}},"file-browser-filebrowser:cwd":{"path":"onlylian"},"editor:onlylian/oar4.py":{"data":{"path":"onlylian/oar4.py","factory":"Editor"}},"editor:docker/v2/Dockerfile":{"data":{"path":"docker/v2/Dockerfile","factory":"Editor"}},"terminal:1":{"data":{"name":"1"}}},"metadata":{"id":"/lab"}} \ No newline at end of file diff --git a/.jupyter/lab/workspaces/labworkspacesauto-c-4c4c.jupyterlab-workspace b/.jupyter/lab/workspaces/labworkspacesauto-c-4c4c.jupyterlab-workspace old mode 100644 new mode 100755 diff --git a/.jupyter/lab/workspaces/labworkspacesauto-d-fa4c.jupyterlab-workspace b/.jupyter/lab/workspaces/labworkspacesauto-d-fa4c.jupyterlab-workspace old mode 100644 new mode 100755 diff --git a/.jupyter/lab/workspaces/labworkspacesauto-m-cb31.jupyterlab-workspace b/.jupyter/lab/workspaces/labworkspacesauto-m-cb31.jupyterlab-workspace old mode 100644 new mode 100755 diff --git a/.jupyter/lab/workspaces/labworkspacesauto-q-6fd4.jupyterlab-workspace b/.jupyter/lab/workspaces/labworkspacesauto-q-6fd4.jupyterlab-workspace old mode 100644 new mode 100755 diff --git a/.jupyter/migrated b/.jupyter/migrated old mode 100644 new mode 100755 diff --git a/.keras/keras.json b/.keras/keras.json new file mode 100755 index 0000000..bc2cae3 --- /dev/null +++ b/.keras/keras.json @@ -0,0 +1,6 @@ +{ + "floatx": "float32", + "epsilon": 1e-07, + "backend": "tensorflow", + "image_data_format": "channels_last" +} \ No newline at end of file diff --git a/.lesshst b/.lesshst new file mode 100755 index 0000000..b8933f4 --- /dev/null +++ b/.lesshst @@ -0,0 +1,8 @@ +.less-history-file: +.search +"nnu +"torch +"opencv +.search +"simpleitk +"SimpleITK diff --git a/.local/share/Trash/info/Untitled Folder.trashinfo b/.local/share/Trash/info/Untitled Folder.trashinfo old mode 100644 new mode 100755 diff --git a/.local/share/Trash/info/resources 1.trashinfo b/.local/share/Trash/info/resources 1.trashinfo old mode 100644 new mode 100755 diff --git a/.local/share/Trash/info/resources.trashinfo b/.local/share/Trash/info/resources.trashinfo old mode 100644 new mode 100755 diff --git a/.local/share/Trash/info/untitled 1.txt.trashinfo b/.local/share/Trash/info/untitled 1.txt.trashinfo old mode 100644 new mode 100755 diff --git a/.local/share/Trash/info/untitled.txt.trashinfo b/.local/share/Trash/info/untitled.txt.trashinfo old mode 100644 new mode 100755 diff --git a/.local/share/jupyter/runtime/nbserver-1-open.html b/.local/share/jupyter/runtime/nbserver-1-open.html old mode 100644 new mode 100755 diff --git a/.local/share/jupyter/runtime/nbserver-1.json b/.local/share/jupyter/runtime/nbserver-1.json old mode 100644 new mode 100755 diff --git a/.local/share/jupyter/runtime/notebook_cookie_secret b/.local/share/jupyter/runtime/notebook_cookie_secret old mode 100644 new mode 100755 diff --git a/.npm/_cacache/content-v2/sha512/1f/cb/884aad9b8d6b5eba9afa7c83fdfbcbe955d786c6c887ad72da30e77d719ea36c7337fc03a5ad4ef79a1d3522eae064bb805bdf301a273a8766da5b21ab5b b/.npm/_cacache/content-v2/sha512/1f/cb/884aad9b8d6b5eba9afa7c83fdfbcbe955d786c6c887ad72da30e77d719ea36c7337fc03a5ad4ef79a1d3522eae064bb805bdf301a273a8766da5b21ab5b old mode 100644 new mode 100755 diff --git a/.npm/_cacache/content-v2/sha512/2c/a8/c457c73791beb8d65d08936a0e7a2a68de1904668ec80c66eae736eacb612d27acc802d2f818b730cf05c4b31a6a19b8745a4a47d6babfdd083035cac131 b/.npm/_cacache/content-v2/sha512/2c/a8/c457c73791beb8d65d08936a0e7a2a68de1904668ec80c66eae736eacb612d27acc802d2f818b730cf05c4b31a6a19b8745a4a47d6babfdd083035cac131 old mode 100644 new mode 100755 diff --git a/.npm/_cacache/content-v2/sha512/4a/a0/69cafd04da71afcf7b38f07beb36e90f43ba42abbf9d4613a666bfa5ba9664069b4a0789e0a3330cf25e8d374417176d7a08b4c9a2cb0dcddcf840db2173 b/.npm/_cacache/content-v2/sha512/4a/a0/69cafd04da71afcf7b38f07beb36e90f43ba42abbf9d4613a666bfa5ba9664069b4a0789e0a3330cf25e8d374417176d7a08b4c9a2cb0dcddcf840db2173 old mode 100644 new mode 100755 diff --git a/.npm/_cacache/content-v2/sha512/53/ab/c1d19a3464a3e96f851cfd247273e83e286bee252eae19b2fed306d1f38053549dc03ca1cd9562e66901b2a28fffb01b51163724e88c06d8a2c6a920bffc b/.npm/_cacache/content-v2/sha512/53/ab/c1d19a3464a3e96f851cfd247273e83e286bee252eae19b2fed306d1f38053549dc03ca1cd9562e66901b2a28fffb01b51163724e88c06d8a2c6a920bffc old mode 100644 new mode 100755 diff --git a/.npm/_cacache/index-v5/2b/cd/3ac6ae314de8839b74075c166d42a7bc667c6a0cdbbfe90261e360fda8bf b/.npm/_cacache/index-v5/2b/cd/3ac6ae314de8839b74075c166d42a7bc667c6a0cdbbfe90261e360fda8bf old mode 100644 new mode 100755 diff --git a/.npm/_cacache/index-v5/4c/64/23f8b5034fecc4dd91aeffce1682eea81041ebd44192c901522303dea70e b/.npm/_cacache/index-v5/4c/64/23f8b5034fecc4dd91aeffce1682eea81041ebd44192c901522303dea70e old mode 100644 new mode 100755 diff --git a/.npm/_cacache/index-v5/ac/d8/02b53e9340cf22cc0e589680dc8ed1731bb9c0adda97021357182d36095a b/.npm/_cacache/index-v5/ac/d8/02b53e9340cf22cc0e589680dc8ed1731bb9c0adda97021357182d36095a old mode 100644 new mode 100755 diff --git a/.npm/_cacache/index-v5/e6/c0/741f8c19375dcfef585384ba651efe7f725e7379ed9fc932281f5a97d9af b/.npm/_cacache/index-v5/e6/c0/741f8c19375dcfef585384ba651efe7f725e7379ed9fc932281f5a97d9af old mode 100644 new mode 100755 diff --git a/.python_history b/.python_history old mode 100644 new mode 100755 index 871bff6..d9baab4 --- a/.python_history +++ b/.python_history @@ -18,3 +18,27 @@ monai._version() monai._version print(monai._version) monai.__version__ +import torch +torch.cuda.is_available() +import torch +torch.cuda.is_available() +nvidia-smi +import torch +torch.cuda.is_available() +import torch +torch.cuda.is_available() +torch.cuda.device_count() +torch.cuda.current_device() +torch.cuda.get_device_name(0) +import pyradise.fileio as ps_io + conv_conf = ps_io.RTSSConverter3DConfiguration() +conv_conf = ps_io.RTSSConverter3DConfiguration() +type(conv_conf) +conv_conf.__class__ +conv_conf.__class__..__name__ +conv_conf.__class__.__name__ +type(conv_conf).__name__ +str(type(conv_conf)) +type(conv_conf).__name__ +from tensorflow.python.client import device_lib +print(device_lib.list_local_devices()) diff --git a/.ssh/known_hosts b/.ssh/known_hosts new file mode 100755 index 0000000..46a8a77 --- /dev/null +++ b/.ssh/known_hosts @@ -0,0 +1 @@ +|1|4vxGI0/l98zY1f4TsRsmEXtNqOA=|FLviQM2mLzejJt7p1mGN+cUuVG4= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBKVS2CCO86WZc9A4GtDx4PIz1Tq91/a2FS8HmgjM/EW diff --git a/.wget-hsts b/.wget-hsts old mode 100644 new mode 100755 index 2b8f815..d1f215b --- a/.wget-hsts +++ b/.wget-hsts @@ -2,4 +2,5 @@ # Edit at your own risk. # raw.githubusercontent.com 0 0 1691530822 31536000 -github.com 0 1 1691530822 31536000 +surfer.nmr.mgh.harvard.edu 0 1 1738329857 31536000 +github.com 0 1 1731403394 31536000 diff --git a/0/dcm2niix b/0/dcm2niix new file mode 100755 index 0000000..29f7374 Binary files /dev/null and b/0/dcm2niix differ diff --git a/0/dcm2niix_lnx.zip b/0/dcm2niix_lnx.zip new file mode 100755 index 0000000..7ebe334 Binary files /dev/null and b/0/dcm2niix_lnx.zip differ diff --git a/0/mri_synthmorph.zip b/0/mri_synthmorph.zip new file mode 100755 index 0000000..9bcde77 Binary files /dev/null and b/0/mri_synthmorph.zip differ diff --git a/3-rtss.py b/3-rtss.py new file mode 100755 index 0000000..eb09cec --- /dev/null +++ b/3-rtss.py @@ -0,0 +1,53 @@ + +import SimpleITK as sitk +from rt_utils import RTStructBuilder + +import numpy as np + +DCM_CT = '/nn/3378417/20241112/CT/8' +NII_CT = "/nn/3378417/20241112/nii/8_1.1_CyberKnife_head(MAR)_20241112110016_4.nii.gz" +label_file = "/nn/3378417/20241112/nii/8_1.1_CyberKnife_head(MAR)_20241112110016.label.nii.gz" +rtss_file = '/nn/3378417/20241112/CT/8-rtss.dcm' + +# reg_transform(NII_CT, NII_MR, output_file, label_file) + +reader = sitk.ImageSeriesReader() +dicom_names = reader.GetGDCMSeriesFileNames(DCM_CT) +reader.SetFileNames(dicom_names) +# reader.SetFileNames(sorted(dicom_names)) +reader.MetaDataDictionaryArrayUpdateOn() +reader.LoadPrivateTagsOn() +image = reader.Execute() +sitk.WriteImage(image, '0/image.nii.gz') + +nnU = sitk.ReadImage(label_file) +nnU = sitk.Resample(nnU, image, sitk.Transform(), sitk.sitkNearestNeighbor) + +sitk.WriteImage(nnU, '0/nnU.nii.gz') + +ccfilter = sitk.ConnectedComponentImageFilter () +nnUCC = ccfilter.Execute(nnU) +ObjectCount1 = ccfilter.GetObjectCount() + +rtstruct = RTStructBuilder.create_new(dicom_series_path=DCM_CT) + +print(ObjectCount1) +for j1 in range(ObjectCount1): + label1 = sitk.BinaryThreshold(nnUCC, j1+1, j1+1) + # label1 = sitk.AntiAliasBinary(label1) + + mask = sitk.GetArrayFromImage(label1).astype(bool) + mask = np.transpose(mask, (1, 2, 0)) + # continue + if mask.any(): + print(j1) + rtstruct.add_roi( + mask=mask, + # use_pin_hole=True, + # name="n%d"%n, + ) + +print(rtss_file) +rtstruct.save(rtss_file) + +# return rtss_file diff --git a/6jj_lths.mat b/6jj_lths.mat deleted file mode 100644 index b09b450..0000000 Binary files a/6jj_lths.mat and /dev/null differ diff --git a/P:/nn/3894670/20241111/nii/6_3D_SAG_T1_MPRAGE_+C_MPR_Tra_20241111141033_13.json b/P:/nn/3894670/20241111/nii/6_3D_SAG_T1_MPRAGE_+C_MPR_Tra_20241111141033_13.json new file mode 100755 index 0000000..84975bc --- /dev/null +++ b/P:/nn/3894670/20241111/nii/6_3D_SAG_T1_MPRAGE_+C_MPR_Tra_20241111141033_13.json @@ -0,0 +1,69 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 1.5, + "ImagingFrequency": 63.6788, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Aera", + "InstitutionName": "NTUH", + "InstitutionalDepartmentName": "Department", + "InstitutionAddress": "Zhongshan_S._Rd._7_Taipei_Zhongzheng_TW_10002", + "DeviceSerialNumber": "141566", + "StationName": "ESIMR01MC01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "cyberknife_head", + "SoftwareVersions": "syngo_MR_E11", + "MRAcquisitionType": "3D", + "SeriesDescription": "3D_SAG_T1_MPRAGE_+C_MPR_Tra", + "ProtocolName": "3D_SAG_T1_MPRAGE_+C_MPR_Tra", + "ScanningSequence": "GR_IR", + "SequenceVariant": "SK_SP_MP_OSP", + "ScanOptions": "IR", + "SequenceName": "_tfl3d1rs16ns", + "ImageType": ["DERIVED", "PRIMARY", "MPR", "NORM", "DIS2D", "SH5", "1", "FIL"], + "RawImage": false, + "SeriesNumber": 13, + "AcquisitionTime": "14:26:11.657500", + "AcquisitionNumber": 1, + "SliceThickness": 1, + "SAR": 0.0200454, + "EchoTime": 0.00461, + "RepetitionTime": 2, + "InversionTime": 0.797, + "FlipAngle": 12, + "PartialFourier": 1, + "BaseResolution": 256, + "ShimSetting": [ + -3760, + 8852, + -7021, + -2213, + -115, + -114, + -133, + 19 ], + "TxRefAmp": 319.32, + "PhaseResolution": 1, + "PhaseOversampling": 0.09, + "ReceiveCoilName": "HeadNeck_20", + "ReceiveCoilActiveElements": "HE1-4;NE1,2", + "CoilString": "HE1-4;NE1_2", + "PulseSequenceDetails": "%SiemensSeq%_tfl", + "ConsistencyInfo": "N4_VE11C_LATEST_20160120", + "PercentPhaseFOV": 100, + "PhaseEncodingSteps": 279, + "AcquisitionMatrixPE": 256, + "ReconMatrixPE": 220, + "PixelBandwidth": 130, + "DwellTime": 1.5e-05, + "ImageOrientationPatientDICOM": [ + 1, + 0, + 0, + 0, + 1, + 0 ], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20181125 (JP2:OpenJPEG) GCC9.3.0" +} diff --git a/P:/nn/3894670/20241111/nii/a_C+_20241111153649_5.json b/P:/nn/3894670/20241111/nii/a_C+_20241111153649_5.json new file mode 100755 index 0000000..5860974 --- /dev/null +++ b/P:/nn/3894670/20241111/nii/a_C+_20241111153649_5.json @@ -0,0 +1,29 @@ +{ + "Modality": "CT", + "Manufacturer": "GE", + "ManufacturersModelName": "Discovery_RT", + "InstitutionName": "NTUH", + "InstitutionAddress": "123", + "StationName": "GERTCT", + "PatientPosition": "HFS", + "ProcedureStepDescription": "3894670", + "SoftwareVersions": "rt_bjcl.83", + "SeriesDescription": "C+", + "ProtocolName": "C+", + "ScanOptions": "HELICAL_MODE", + "ImageType": ["ORIGINAL", "PRIMARY", "AXIAL"], + "SeriesNumber": 5, + "AcquisitionTime": "15:44:55.235500", + "AcquisitionNumber": 1, + "XRayExposure": 53, + "ReconMatrixPE": 512, + "ImageOrientationPatientDICOM": [ + 1, + 0, + 0, + 0, + 1, + 0 ], + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20181125 (JP2:OpenJPEG) GCC9.3.0" +} diff --git a/adding.py b/adding.py old mode 100644 new mode 100755 diff --git a/bcngawwk.mat b/bcngawwk.mat deleted file mode 100644 index 4104c26..0000000 Binary files a/bcngawwk.mat and /dev/null differ diff --git a/docker/v1.5/Dockerfile b/docker/v1.5/Dockerfile new file mode 100755 index 0000000..f3d0682 --- /dev/null +++ b/docker/v1.5/Dockerfile @@ -0,0 +1,63 @@ +# FROM nvcr.io/nvidia/clara-train-sdk:v4.1 +FROM nvcr.io/nvidia/pytorch:22.08-py3 + + +# apt +RUN apt-get update -y \ +&& apt-get install libxrender1 openssh-server pigz sudo -y + +#conda +# RUN conda update -y -n base conda +# RUN conda install -y -n base conda-libmamba-solver +# RUN conda config -y --set solver libmamba + +# pip +# RUN pip install --upgrade pip + +# RUN pip install antspyx itk-elastix nipype nnunet rt_utils +RUN pip install antspyx nipype nnunet nnunetv2 simpleitk==2.3.1 tensorflow # too large .... install first +# RUN pip install itk-elastix==0.13.0 +# RUN pip install itk-elastix +# RUN pip install --upgrade git+https://github.com/FabianIsensee/hiddenlayer.git@more_plotted_details#egg=hiddenlayer + +# RUN pip install --upgrade git+https://github.com/FabianIsensee/hiddenlayer.git +# RUN pip install --upgrade git+https://github.com/rohitrango/fireants.git +# RUN pip install git+https://github.com/qurit/rt-utils.git@5bab9ffcc8fe19dd775e940afdc3d8f48f869150 # fix FrameOfReferenceUID + +# RUN pip install git+https://github.com/adalca/neurite.git git+https://github.com/freesurfer/surfa.git git+https://github.com/voxelmorph/voxelmorph.git + +# WORKDIR /workspace +WORKDIR /123 +COPY requirements.txt /123 +RUN pip install -r requirements.txt + +RUN wget https://github.com/rordenlab/dcm2niix/releases/download/v1.0.20240202/dcm2niix_lnx.zip +RUN unzip dcm2niix_lnx.zip +RUN cp dcm2niix /usr/bin + +# nnUNet + +ENV nnUNet_raw_data_base="/workspace/nnUNet_raw_data_base" +ENV nnUNet_preprocessed="/workspace/nnUNet_preprocessed" +ENV RESULTS_FOLDER="/workspace/nnUNet_trained_models" + +ENV nnUNet_raw="/workspace/v2/nnUNet_raw" +ENV nnUNet_preprocessed="/workspace/v2/nnUNet_preprocessed" +ENV nnUNet_results="/workspace/v2/nnUNet_results" + +# SSH server +#RUN echo 'root:password' | chpasswd +#RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config +#RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config +#ENTRYPOINT service ssh restart && env >> /etc/environment && bash +#EXPOSE 22 + +# Masonite +EXPOSE 8000 + +# jupyter +ENTRYPOINT jupyter-lab +EXPOSE 8888 + +# pynetdicom +EXPOSE 11120 diff --git a/docker/v1.5/build.sh b/docker/v1.5/build.sh new file mode 100755 index 0000000..9a5c207 --- /dev/null +++ b/docker/v1.5/build.sh @@ -0,0 +1,2 @@ +# DOCKER_BUILDKIT=1 docker build -t 123:v2 . +docker build -t 123:v2 . diff --git a/docker/v1.5/qrun.sh b/docker/v1.5/qrun.sh new file mode 100755 index 0000000..26c66cd --- /dev/null +++ b/docker/v1.5/qrun.sh @@ -0,0 +1,30 @@ + +docker stop 123 +docker rm 123 + +#DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +#DIR="$(dirname $( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd ))" + +export dockerImage=123:v2 + +#docker run --gpus all --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 -it --rm -v $DIR:/workspace $dockerImage /bin/bash +# GPU=nvidia0 gpu-docker run --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 -it --rm \ +#GPU=nvidia0 gpu-docker run --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \ + + + +docker run --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \ +--name 123 \ +--volume=/share/CACHEDEV1_DATA/.qpkg/NVIDIA_GPU_DRV/usr:/usr/local/nvidia \ +--device /dev/nvidiactl:/dev/nvidiactl --device /dev/nvidia-uvm:/dev/nvidia-uvm --device /dev/nvidia0:/dev/nvidia0 \ +-v /share/WORKSPACE/nnUNet:/workspace \ +-v /share/Public/git/123:/123 \ +-v /share/Public/git/123:/root \ +-v /share/Public/nn:/nn \ +-v /share/Public/Patient:/Patient \ +-p 8000:8000 \ +-p 8888:8888 \ +-p 11120:11120 \ +--restart=always \ +$dockerImage & + diff --git a/docker/v1.5/requirements.txt b/docker/v1.5/requirements.txt new file mode 100755 index 0000000..d8c925f --- /dev/null +++ b/docker/v1.5/requirements.txt @@ -0,0 +1,16 @@ +masonite>=4.0,<5.0 +masonite-orm>=2.0,<3.0 + +opencv-python<4.6 # for rt-utils + +# fireants +itk-elastix +pynetdicom +pyradise +rt-utils + +git+https://github.com/FabianIsensee/hiddenlayer.git + +git+https://github.com/adalca/neurite.git +git+https://github.com/freesurfer/surfa.git +git+https://github.com/voxelmorph/voxelmorph.git diff --git a/docker/Dockerfile b/docker/v1/Dockerfile similarity index 82% rename from docker/Dockerfile rename to docker/v1/Dockerfile index fafe72e..146bda3 100755 --- a/docker/Dockerfile +++ b/docker/v1/Dockerfile @@ -2,7 +2,7 @@ FROM nvcr.io/nvidia/clara-train-sdk:v4.1 # apt RUN apt-get update -y \ -&& apt-get install dcm2niix openssh-server pigz sudo -y +&& apt-get install openssh-server pigz sudo -y # pip # RUN pip install antspyx itk-elastix nipype nnunet rt_utils @@ -17,6 +17,10 @@ WORKDIR /123 COPY requirements.txt /123 RUN pip install -r requirements.txt +RUN wget https://github.com/rordenlab/dcm2niix/releases/download/v1.0.20240202/dcm2niix_lnx.zip +RUN unzip dcm2niix_lnx.zip +RUN cp dcm2niix /usr/bin + # nnUNet ENV nnUNet_raw_data_base="/workspace/nnUNet_raw_data_base" diff --git a/docker/build.sh b/docker/v1/build.sh similarity index 100% rename from docker/build.sh rename to docker/v1/build.sh diff --git a/docker/qrun.sh b/docker/v1/qrun.sh similarity index 76% rename from docker/qrun.sh rename to docker/v1/qrun.sh index 856bc9c..17d054e 100755 --- a/docker/qrun.sh +++ b/docker/v1/qrun.sh @@ -1,3 +1,7 @@ + +docker stop 123 +docker rm 123 + #DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" #DIR="$(dirname $( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd ))" @@ -6,8 +10,13 @@ export dockerImage=123:v0 #docker run --gpus all --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 -it --rm -v $DIR:/workspace $dockerImage /bin/bash # GPU=nvidia0 gpu-docker run --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 -it --rm \ #GPU=nvidia0 gpu-docker run --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \ + + + docker run --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \ --name 123 \ +--volume=/share/CACHEDEV1_DATA/.qpkg/NVIDIA_GPU_DRV/usr:/usr/local/nvidia \ +--device /dev/nvidiactl:/dev/nvidiactl --device /dev/nvidia-uvm:/dev/nvidia-uvm --device /dev/nvidia0:/dev/nvidia0 \ -v /share/WORKSPACE/nnUNet:/workspace \ -v /share/Public/git/123:/123 \ -v /share/Public/git/123:/root \ @@ -16,8 +25,6 @@ docker run --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \ -p 8000:8000 \ -p 8888:8888 \ -p 11120:11120 \ +--restart=always \ $dockerImage & - -# Then recreate, adding GPU in runtime - diff --git a/docker/requirements.txt b/docker/v1/requirements.txt old mode 100644 new mode 100755 similarity index 100% rename from docker/requirements.txt rename to docker/v1/requirements.txt diff --git a/docker/v2/Dockerfile b/docker/v2/Dockerfile new file mode 100755 index 0000000..8c48a23 --- /dev/null +++ b/docker/v2/Dockerfile @@ -0,0 +1,51 @@ +# FROM nvcr.io/nvidia/clara-train-sdk:v4.1 +# FROM nvcr.io/nvidia/pytorch:22.08-py3 +# FROM nvcr.io/nvidia/pytorch:24.01-py3 +FROM nvcr.io/nvidia/tensorflow:24.03-tf2-py3 + +# apt +RUN apt-get update -y \ +&& apt-get install libxrender1 openssh-server pigz sudo -y + +# pip + +# RUN pip install antspyx itk-elastix nipype nnunet nnunetv2 simpleitk==2.3.1 tensorflow[and-cuda] #too large .... install first + +RUN pip install "torch<2.6" torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 +RUN pip install antspyx itk-elastix nipype nnunet nnunetv2 simpleitk==2.3.1 #too large .... install first + +# WORKDIR /workspace +WORKDIR /123 +COPY requirements.txt /123 +RUN pip install -r requirements.txt + +RUN wget https://github.com/rordenlab/dcm2niix/releases/download/v1.0.20240202/dcm2niix_lnx.zip +RUN unzip dcm2niix_lnx.zip +RUN cp dcm2niix /usr/bin + +# nnUNet + +ENV nnUNet_raw_data_base="/workspace/nnUNet_raw_data_base" +ENV nnUNet_preprocessed="/workspace/nnUNet_preprocessed" +ENV RESULTS_FOLDER="/workspace/nnUNet_trained_models" + +ENV nnUNet_raw="/workspace/v2/nnUNet_raw" +ENV nnUNet_preprocessed="/workspace/v2/nnUNet_preprocessed" +ENV nnUNet_results="/workspace/v2/nnUNet_results" + +# SSH server +#RUN echo 'root:password' | chpasswd +#RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config +#RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config +#ENTRYPOINT service ssh restart && env >> /etc/environment && bash +#EXPOSE 22 + +# Masonite +EXPOSE 8000 + +# jupyter +ENTRYPOINT jupyter-lab +EXPOSE 8888 + +# pynetdicom +EXPOSE 11120 diff --git a/docker/v2/build.sh b/docker/v2/build.sh new file mode 100755 index 0000000..9a5c207 --- /dev/null +++ b/docker/v2/build.sh @@ -0,0 +1,2 @@ +# DOCKER_BUILDKIT=1 docker build -t 123:v2 . +docker build -t 123:v2 . diff --git a/docker/v2/qrun.sh b/docker/v2/qrun.sh new file mode 100755 index 0000000..26c66cd --- /dev/null +++ b/docker/v2/qrun.sh @@ -0,0 +1,30 @@ + +docker stop 123 +docker rm 123 + +#DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +#DIR="$(dirname $( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd ))" + +export dockerImage=123:v2 + +#docker run --gpus all --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 -it --rm -v $DIR:/workspace $dockerImage /bin/bash +# GPU=nvidia0 gpu-docker run --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 -it --rm \ +#GPU=nvidia0 gpu-docker run --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \ + + + +docker run --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \ +--name 123 \ +--volume=/share/CACHEDEV1_DATA/.qpkg/NVIDIA_GPU_DRV/usr:/usr/local/nvidia \ +--device /dev/nvidiactl:/dev/nvidiactl --device /dev/nvidia-uvm:/dev/nvidia-uvm --device /dev/nvidia0:/dev/nvidia0 \ +-v /share/WORKSPACE/nnUNet:/workspace \ +-v /share/Public/git/123:/123 \ +-v /share/Public/git/123:/root \ +-v /share/Public/nn:/nn \ +-v /share/Public/Patient:/Patient \ +-p 8000:8000 \ +-p 8888:8888 \ +-p 11120:11120 \ +--restart=always \ +$dockerImage & + diff --git a/docker/v2/requirements.txt b/docker/v2/requirements.txt new file mode 100755 index 0000000..0abd742 --- /dev/null +++ b/docker/v2/requirements.txt @@ -0,0 +1,14 @@ +#masonite>=4.0,<5.0 +#masonite-orm>=2.0,<3.0 + +opencv-python<4.6 # for rt-utils + +pynetdicom +pyradise +rt-utils + +git+https://github.com/FabianIsensee/hiddenlayer.git + +git+https://github.com/adalca/neurite.git +git+https://github.com/freesurfer/surfa.git +git+https://github.com/voxelmorph/voxelmorph.git diff --git a/fireants b/fireants new file mode 160000 index 0000000..d363d82 --- /dev/null +++ b/fireants @@ -0,0 +1 @@ +Subproject commit d363d8240835f968e872e462b83ea461a1d57f4b diff --git a/myapp/.env-example b/myapp/.env-example old mode 100644 new mode 100755 diff --git a/myapp/.env.testing b/myapp/.env.testing old mode 100644 new mode 100755 diff --git a/myapp/.gitignore b/myapp/.gitignore old mode 100644 new mode 100755 diff --git a/myapp/Kernel.py b/myapp/Kernel.py old mode 100644 new mode 100755 diff --git a/myapp/app/__init__.py b/myapp/app/__init__.py old mode 100644 new mode 100755 diff --git a/myapp/app/controllers/BlogController.py b/myapp/app/controllers/BlogController.py old mode 100644 new mode 100755 diff --git a/myapp/app/controllers/PatientController.py b/myapp/app/controllers/PatientController.py old mode 100644 new mode 100755 diff --git a/myapp/app/controllers/WelcomeController.py b/myapp/app/controllers/WelcomeController.py old mode 100644 new mode 100755 diff --git a/myapp/app/controllers/__init__.py b/myapp/app/controllers/__init__.py old mode 100644 new mode 100755 diff --git a/myapp/app/controllers/api/UsersController.py b/myapp/app/controllers/api/UsersController.py old mode 100644 new mode 100755 diff --git a/myapp/app/middlewares/AuthenticationMiddleware.py b/myapp/app/middlewares/AuthenticationMiddleware.py old mode 100644 new mode 100755 diff --git a/myapp/app/middlewares/VerifyCsrfToken.py b/myapp/app/middlewares/VerifyCsrfToken.py old mode 100644 new mode 100755 diff --git a/myapp/app/middlewares/__init__.py b/myapp/app/middlewares/__init__.py old mode 100644 new mode 100755 diff --git a/myapp/app/models/User.py b/myapp/app/models/User.py old mode 100644 new mode 100755 diff --git a/myapp/app/providers/AppProvider.py b/myapp/app/providers/AppProvider.py old mode 100644 new mode 100755 diff --git a/myapp/app/providers/__init__.py b/myapp/app/providers/__init__.py old mode 100644 new mode 100755 diff --git a/myapp/config/__init__.py b/myapp/config/__init__.py old mode 100644 new mode 100755 diff --git a/myapp/config/api.py b/myapp/config/api.py old mode 100644 new mode 100755 diff --git a/myapp/config/application.py b/myapp/config/application.py old mode 100644 new mode 100755 diff --git a/myapp/config/auth.py b/myapp/config/auth.py old mode 100644 new mode 100755 diff --git a/myapp/config/broadcast.py b/myapp/config/broadcast.py old mode 100644 new mode 100755 diff --git a/myapp/config/cache.py b/myapp/config/cache.py old mode 100644 new mode 100755 diff --git a/myapp/config/database.py b/myapp/config/database.py old mode 100644 new mode 100755 diff --git a/myapp/config/exceptions.py b/myapp/config/exceptions.py old mode 100644 new mode 100755 diff --git a/myapp/config/filesystem.py b/myapp/config/filesystem.py old mode 100644 new mode 100755 diff --git a/myapp/config/mail.py b/myapp/config/mail.py old mode 100644 new mode 100755 diff --git a/myapp/config/notification.py b/myapp/config/notification.py old mode 100644 new mode 100755 diff --git a/myapp/config/providers.py b/myapp/config/providers.py old mode 100644 new mode 100755 diff --git a/myapp/config/queue.py b/myapp/config/queue.py old mode 100644 new mode 100755 diff --git a/myapp/config/session.py b/myapp/config/session.py old mode 100644 new mode 100755 diff --git a/myapp/craft b/myapp/craft old mode 100644 new mode 100755 diff --git a/myapp/databases/migrations/2021_01_09_033202_create_password_reset_table.py b/myapp/databases/migrations/2021_01_09_033202_create_password_reset_table.py old mode 100644 new mode 100755 diff --git a/myapp/databases/migrations/2021_01_09_043202_create_users_table.py b/myapp/databases/migrations/2021_01_09_043202_create_users_table.py old mode 100644 new mode 100755 diff --git a/myapp/databases/seeds/__init__.py b/myapp/databases/seeds/__init__.py old mode 100644 new mode 100755 diff --git a/myapp/databases/seeds/database_seeder.py b/myapp/databases/seeds/database_seeder.py old mode 100644 new mode 100755 diff --git a/myapp/databases/seeds/user_table_seeder.py b/myapp/databases/seeds/user_table_seeder.py old mode 100644 new mode 100755 diff --git a/myapp/makefile b/myapp/makefile old mode 100644 new mode 100755 diff --git a/myapp/package.json b/myapp/package.json old mode 100644 new mode 100755 diff --git a/myapp/pyproject.toml b/myapp/pyproject.toml old mode 100644 new mode 100755 diff --git a/myapp/requirements.txt b/myapp/requirements.txt old mode 100644 new mode 100755 diff --git a/myapp/resources/css/app.css b/myapp/resources/css/app.css old mode 100644 new mode 100755 diff --git a/myapp/resources/js/app.js b/myapp/resources/js/app.js old mode 100644 new mode 100755 diff --git a/myapp/resources/js/bootstrap.js b/myapp/resources/js/bootstrap.js old mode 100644 new mode 100755 diff --git a/myapp/routes/api.py b/myapp/routes/api.py old mode 100644 new mode 100755 diff --git a/myapp/routes/web.py b/myapp/routes/web.py old mode 100644 new mode 100755 diff --git a/myapp/setup.cfg b/myapp/setup.cfg old mode 100644 new mode 100755 diff --git a/myapp/storage/.gitignore b/myapp/storage/.gitignore old mode 100644 new mode 100755 diff --git a/myapp/storage/public/favicon.ico b/myapp/storage/public/favicon.ico old mode 100644 new mode 100755 diff --git a/myapp/storage/public/logo.png b/myapp/storage/public/logo.png old mode 100644 new mode 100755 diff --git a/myapp/storage/public/robots.txt b/myapp/storage/public/robots.txt old mode 100644 new mode 100755 diff --git a/myapp/templates/__init__.py b/myapp/templates/__init__.py old mode 100644 new mode 100755 diff --git a/myapp/templates/base-tailwind.html b/myapp/templates/base-tailwind.html old mode 100644 new mode 100755 diff --git a/myapp/templates/base.html b/myapp/templates/base.html old mode 100644 new mode 100755 diff --git a/myapp/templates/blog.html b/myapp/templates/blog.html old mode 100644 new mode 100755 diff --git a/myapp/templates/errors/403.html b/myapp/templates/errors/403.html old mode 100644 new mode 100755 diff --git a/myapp/templates/errors/404.html b/myapp/templates/errors/404.html old mode 100644 new mode 100755 diff --git a/myapp/templates/errors/500.html b/myapp/templates/errors/500.html old mode 100644 new mode 100755 diff --git a/myapp/templates/maintenance.html b/myapp/templates/maintenance.html old mode 100644 new mode 100755 diff --git a/myapp/templates/patient.html b/myapp/templates/patient.html old mode 100644 new mode 100755 diff --git a/myapp/templates/welcome.html b/myapp/templates/welcome.html old mode 100644 new mode 100755 diff --git a/myapp/tests/TestCase.py b/myapp/tests/TestCase.py old mode 100644 new mode 100755 diff --git a/myapp/tests/__init__.py b/myapp/tests/__init__.py old mode 100644 new mode 100755 diff --git a/myapp/tests/unit/test_basic_testcase.py b/myapp/tests/unit/test_basic_testcase.py old mode 100644 new mode 100755 diff --git a/myapp/webpack.mix.js b/myapp/webpack.mix.js old mode 100644 new mode 100755 diff --git a/myapp/wsgi.py b/myapp/wsgi.py old mode 100644 new mode 100755 diff --git a/oar.py.save b/oar.py.save new file mode 100755 index 0000000..3b55231 --- /dev/null +++ b/oar.py.save @@ -0,0 +1,451 @@ +""" +醫學影像處理程式 +功能:執行 CT 和 MR 影像的處理、轉換和 RTSTRUCT 生成 + +使用方式: +1. 命令列執行: + python oar.py /nn/3894670/20241111/CT/a /nn/3894670/20241111/MR/6 + +2. Jupyter Notebook 執行: + from oar import inference, SendDCM + rtss_file = inference("/nn/3894670/20241111/CT/a", "/nn/3894670/20241111/MR/6") + SendDCM(rtss_file) +""" + +import os +import shutil +import subprocess +import sys +import time +import logging +from pathlib import Path + +from nipype.interfaces.dcm2nii import Dcm2niix +from pydicom import dcmread +from pynetdicom import AE, debug_logger +from pynetdicom.sop_class import CTImageStorage, RTStructureSetStorage +from rt_utils import RTStructBuilder + +import numpy as np +import SimpleITK as sitk + +from registration.best_reg import reg_transform + +# 設定日誌 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('medical_imaging.log') + ] +) +logger = logging.getLogger(__name__) + +def validate_dicom_directory(dicom_path): + """ + 加強版 DICOM 目錄驗證 + + 參數: + dicom_path: str - DICOM 目錄路徑 + + 回傳: + tuple: (is_valid: bool, message: str) + """ + try: + if not os.path.exists(dicom_path): + return False, f"目錄不存在:{dicom_path}" + + if not os.listdir(dicom_path): + return False, f"目錄是空的:{dicom_path}" + + dicom_files = [] + non_dicom = [] + corrupt_files = [] + series_info = {} + + for filename in os.listdir(dicom_path): + filepath = os.path.join(dicom_path, filename) + if os.path.isfile(filepath): + try: + ds = dcmread(filepath) + # 檢查必要的 DICOM 標籤 + if hasattr(ds, 'SeriesInstanceUID'): + series_uid = ds.SeriesInstanceUID + if series_uid not in series_info: + series_info[series_uid] = [] + series_info[series_uid].append(filepath) + dicom_files.append(filepath) + else: + non_dicom.append(filename) + except Exception as e: + logger.warning(f"無法讀取檔案 {filename}: {str(e)}") + if filename.endswith(('.dcm', '.DCM')): + corrupt_files.append(filename) + else: + non_dicom.append(filename) + + if not dicom_files: + message = f"在 {dicom_path} 中找不到有效的 DICOM 檔案\n" + if non_dicom: + message += f"發現 {len(non_dicom)} 個非 DICOM 檔案\n" + if corrupt_files: + message += f"發現 {len(corrupt_files)} 個損壞的 DICOM 檔案" + return False, message + + # 檢查是否所有檔案屬於同一個系列 + if len(series_info) > 1: + logger.warning(f"發現多個系列: {len(series_info)}") + + # 檢查檔案數量是否合理 + for series_uid, files in series_info.items(): + if len(files) < 5: # 假設至少需要5張影像 + logger.warning(f"系列 {series_uid} 的影像數量過少: {len(files)}") + + return True, f"找到 {len(dicom_files)} 個有效的 DICOM 檔案,分屬 {len(series_info)} 個系列" + + except Exception as e: + return False, f"驗證過程發生錯誤:{str(e)}" + +def enhanced_dcm2nii(source_dir, output_dir): + """ + 加強版的 dcm2nii 轉換函式 + + 參數: + source_dir: str - 來源 DICOM 目錄 + output_dir: str - NIfTI 檔案的輸出目錄 + + 回傳: + tuple: (success: bool, message: str, nifti_files: list) + """ + try: + # 驗證輸入目錄 + is_valid, message = validate_dicom_directory(source_dir) + if not is_valid: + return False, message, [] + + # 確保輸出目錄存在 + os.makedirs(output_dir, exist_ok=True) + + # 記錄轉換前的檔案 + initial_files = set(os.listdir(output_dir)) + + # 設定 dcm2niix 的進階選項 + converter = Dcm2niix() + converter.inputs.source_dir = source_dir + converter.inputs.output_dir = output_dir + converter.inputs.compress = 'y' + converter.inputs.merge_imgs = True + converter.inputs.single_file = True + converter.inputs.verbose = True + converter.terminal_output = 'stream' + + logger.info(f"執行 DICOM 轉換:{source_dir}") + try: + result = converter.run() + + # 輸出 dcm2niix 的詳細日誌 + if hasattr(result, 'runtime'): + if hasattr(result.runtime, 'stdout'): + logger.info(f"dcm2niix 輸出:\n{result.runtime.stdout}") + if hasattr(result.runtime, 'stderr'): + logger.warning(f"dcm2niix 錯誤:\n{result.runtime.stderr}") + except Exception as run_error: + logger.error(f"dcm2niix 執行錯誤:{str(run_error)}") + + # 檢查新產生的檔案 + final_files = set(os.listdir(output_dir)) + new_files = final_files - initial_files + nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + + if not nifti_files: + # 嘗試使用備用轉換選項 + logger.warning("使用備用轉換選項重試...") + converter.inputs.merge_imgs = False + converter.inputs.single_file = False + try: + result = converter.run() + except Exception as retry_error: + logger.error(f"備用轉換選項執行錯誤:{str(retry_error)}") + + final_files = set(os.listdir(output_dir)) + new_files = final_files - initial_files + nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + + if not nifti_files: + return False, "轉換完成但未產生 NIfTI 檔案", [] + + # 驗證產生的 NIfTI 檔案 + valid_nifti = [] + for nii_file in nifti_files: + try: + img = sitk.ReadImage(os.path.join(output_dir, nii_file)) + if img.GetSize()[2] > 1: # 確保是3D影像 + valid_nifti.append(nii_file) + else: + logger.warning(f"檔案 {nii_file} 不是有效的3D影像") + except Exception as e: + logger.warning(f"無法讀取 NIfTI 檔案 {nii_file}: {str(e)}") + + if not valid_nifti: + return False, "轉換產生的 NIfTI 檔案無效", [] + + return True, f"成功轉換 {len(valid_nifti)} 個有效的 NIfTI 檔案", valid_nifti + + except Exception as e: + logger.error(f"DICOM 轉換錯誤:{str(e)}", exc_info=True) + return False, f"轉換過程發生錯誤:{str(e)}", [] + +def inference(DCM_CT, DCM_MR): + """ + 執行影像推論和後處理 + + 參數: + DCM_CT: str - CT 影像的 DICOM 目錄路徑 + DCM_MR: str - MR 影像的 DICOM 目錄路徑 + + 回傳: + str: 生成的 RTSTRUCT 檔案路徑 + """ + logger.info(f"處理影像\nCT: {DCM_CT}\nMR: {DCM_MR}") + + try: + # 驗證輸入目錄 + for path, desc in [(DCM_CT, "CT"), (DCM_MR, "MR")]: + is_valid, message = validate_dicom_directory(path) + if not is_valid: + raise ValueError(f"{desc} 影像驗證失敗:{message}") + + # 設定工作目錄 + ROOT_DIR = os.path.dirname(os.path.dirname(DCM_CT)) + NII_DIR = os.path.join(ROOT_DIR, 'nii') + INPUT_DIR = os.path.join(ROOT_DIR, 'input') + OUTPUT_DIR = os.path.join(ROOT_DIR, 'output') + + # 設定 RTSTRUCT 輸出路徑 + head, tail = os.path.split(DCM_CT) + rtss_file = os.path.join(head, tail+'-rtss.dcm') + + # 清理並建立工作目錄 + for dir_path in [NII_DIR, INPUT_DIR, OUTPUT_DIR]: + shutil.rmtree(dir_path, ignore_errors=True) + os.makedirs(dir_path, exist_ok=True) + + # 取得基本檔名 + nCT = os.path.basename(DCM_CT) + nMR = os.path.basename(DCM_MR) + + # DICOM 轉 NIfTI + logger.info("開始轉換 CT DICOM...") + success_ct, message_ct, ct_files = enhanced_dcm2nii(DCM_CT, NII_DIR) + if not success_ct: + raise RuntimeError(f"CT 轉換失敗:{message_ct}") + + logger.info("開始轉換 MR DICOM...") + success_mr, message_mr, mr_files = enhanced_dcm2nii(DCM_MR, NII_DIR) + if not success_mr: + raise RuntimeError(f"MR 轉換失敗:{message_mr}") + + # 尋找轉換後的檔案 + NII_CT = None + NII_MR = None + for f in os.listdir(NII_DIR): + if f.endswith('.nii.gz'): + full_path = os.path.join(NII_DIR, f) + if f.startswith(nCT+'_'): + NII_CT = full_path + logger.info(f"找到 CT NIfTI 檔案:{f}") + elif f.startswith(nMR+'_'): + NII_MR = full_path + logger.info(f"找到 MR NIfTI 檔案:{f}") + + if not NII_CT: + raise FileNotFoundError(f"找不到 CT 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + if not NII_MR: + raise FileNotFoundError(f"找不到 MR 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + + # 準備輸入檔案 + basename = os.path.basename(NII_MR) + old = '_'+basename.split('_')[-1] + input_file = os.path.join(INPUT_DIR, basename.replace(old, '_0000.nii.gz')) + output_file = os.path.join(OUTPUT_DIR, basename.replace(old, '.nii.gz')) + + basename_ct = os.path.basename(NII_CT) + old_ct = '_'+basename_ct.split('_')[-1] + label_file = os.path.join(NII_DIR, basename_ct.replace(old_ct, '.label.nii.gz')) + + logger.info(f"複製 MR NIfTI 到輸入目錄:{input_file}") + shutil.copy(NII_MR, input_file) + + logger.info("準備執行模型推論...") + try: + # 執行模型推論 + subprocess.run([ + "nnUNetv2_predict", + "-i", INPUT_DIR, + "-o", OUTPUT_DIR, + "-m", "2d", + "-f", "0", + "-tr", "nnUNetTrainer", + "-p", "nnUNetPlans", + "-t", "501", + "--save_npz" + ], check=True, capture_output=True, text=True) + + except subprocess.CalledProcessError as e: + logger.error(f"模型推論失敗:\n輸出:{e.output}\n錯誤:{e.stderr}") + raise RuntimeError("模型推論失敗,請檢查日誌獲取詳細資訊") + + logger.info(f"模型推論完成:{output_file}") + + if not os.path.exists(output_file): + raise FileNotFoundError(f"找不到模型輸出檔案:{output_file}") + + logger.info("開始執行影像配準...") + + # 影像配準 + try: + reg_transform(NII_CT, NII_MR, output_file, label_file) + except Exception as e: + logger.error(f"影像配準失敗:{str(e)}") + raise RuntimeError("影像配準失敗") + + if not os.path.exists(label_file): + raise FileNotFoundError(f"找不到配準後的標籤檔案:{label_file}") + + logger.info("開始建立 RTSTRUCT...") + + # 讀取 CT 影像序列 + try: + reader = sitk.ImageSeriesReader() + dicom_names = reader.GetGDCMSeriesFileNames(DCM_CT) + reader.SetFileNames(dicom_names) + reader.MetaDataDictionaryArrayUpdateOn() + reader.LoadPrivateTagsOn() + image = reader.Execute() + except Exception as e: + logger.error(f"讀取 CT 系列失敗:{str(e)}") + raise RuntimeError("無法讀取 CT 影像系列") + + # 重新取樣預測結果 + try: + nnU = sitk.ReadImage(label_file) + nnU = sitk.Resample(nnU, image, sitk.Transform(), sitk.sitkNearestNeighbor) + except Exception as e: + logger.error(f"重新取樣失敗:{str(e)}") + raise RuntimeError("預測結果重新取樣失敗") + + # 連通元件分析 + try: + ccfilter = sitk.ConnectedComponentImageFilter() + nnUCC = ccfilter.Execute(nnU) + ObjectCount1 = ccfilter.GetObjectCount() + + if ObjectCount1 == 0: + logger.warning("未找到任何連通元件") + except Exception as e: + logger.error(f"連通元件分析失敗:{str(e)}") + raise RuntimeError("連通元件分析失敗") + + # 建立 RTSTRUCT + try: + rtstruct = RTStructBuilder.create_new(dicom_series_path=DCM_CT) + + # 處理每個連通元件 + for j1 in range(ObjectCount1): + label1 = sitk.BinaryThreshold(nnUCC, j1+1, j1+1) + mask = sitk.GetArrayFromImage(label1).astype(bool) + mask = np.transpose(mask, (1, 2, 0)) + if mask.any(): + logger.info(f"處理 ROI {j1+1}/{ObjectCount1}") + rtstruct.add_roi( + mask=mask, + color=[255, 0, 0], + name=f"Tumor_{j1+1}" + ) + + logger.info(f"儲存 RTSTRUCT:{rtss_file}") + rtstruct.save(rtss_file) + except Exception as e: + logger.error(f"RTSTRUCT 生成失敗:{str(e)}") + raise RuntimeError("無法生成或儲存 RTSTRUCT") + + return rtss_file + + except Exception as e: + logger.error(f"推論過程發生錯誤:{str(e)}", exc_info=True) + raise + +def SendDCM(fp): + """ + 傳送 DICOM 檔案到 PACS + + 參數: + fp: str - DICOM 檔案路徑 + """ + logger.info(f"準備傳送 DICOM 檔案:{fp}") + debug_logger() + + if not os.path.exists(fp): + raise FileNotFoundError(f"找不到 DICOM 檔案:{fp}") + + try: + ae = AE() + ae.ae_title = 'OUR_STORE_SCP' + ae.add_requested_context(RTStructureSetStorage) + + try: + ds = dcmread(fp) + except Exception as e: + logger.error(f"讀取 DICOM 檔案失敗:{str(e)}") + raise RuntimeError("無法讀取 DICOM 檔案") + + logger.info("正在連接 PACS 伺服器...") + try: + assoc = ae.associate("172.16.40.36", 104, ae_title='N1000_STORAGE') + except Exception as e: + logger.error(f"PACS 連接失敗:{str(e)}") + raise RuntimeError("無法連接 PACS 伺服器") + + if assoc.is_established: + try: + status = assoc.send_c_store(ds) + if status: + logger.info(f'DICOM 傳送成功,狀態碼: 0x{status.Status:04x}') + else: + raise RuntimeError('傳送失敗:連接逾時或收到無效回應') + except Exception as e: + logger.error(f"DICOM 傳送失敗:{str(e)}") + raise + finally: + assoc.release() + else: + raise RuntimeError('傳送失敗:無法建立連接') + + except Exception as e: + logger.error(f"DICOM 傳送過程發生錯誤:{str(e)}", exc_info=True) + raise + +def main(): + """主程式進入點""" + if len(sys.argv) < 3: + print('使用方式:', sys.argv[0], ' ') + print('範例: python oar.py /nn/3894670/20241111/CT/a /nn/3894670/20241111/MR/6') + sys.exit(1) + + logger.info('開始執行') + logger.info('程式名稱: %s', sys.argv[0]) + logger.info('CT路徑: %s', sys.argv[1]) + logger.info('MR路徑: %s', sys.argv[2]) + + start_time = time.time() + try: + rtss_file = inference(sys.argv[1], sys.argv[2]) + SendDCM(rtss_file) + logger.info(f"處理完成,總耗時:{time.time() - start_time:.2f} 秒") + except Exception as e: + logger.error(f"處理過程發生錯誤:{str(e)}", exc_info=True) + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/old/itk_elastix.py b/old/itk_elastix.py old mode 100644 new mode 100755 diff --git a/onlylian/0/trans.lta b/onlylian/0/trans.lta new file mode 100755 index 0000000..a4f5078 --- /dev/null +++ b/onlylian/0/trans.lta @@ -0,0 +1,27 @@ +type = 1 # LINEAR_RAS_TO_RAS +nxforms = 1 +mean = 0.0000 0.0000 0.0000 +sigma = 1.0000 +1 4 4 +9.681913151996967e-01 -3.489692401987950e-02 -6.076559461674157e-03 -1.222032955258101e+00 +3.105444924121966e-02 9.654058265323426e-01 -7.219475638798915e-02 3.684411494177422e+01 +1.357010239462778e-02 2.781454867708578e-02 9.983539936071616e-01 -2.475070371764079e+01 +0.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00 1.000000000000000e+00 +src volume info +valid = 1 +filename = none +volume = 256 256 180 +voxelsize = 9.690272212028503e-01 9.690272212028503e-01 1.100000739097595e+00 +xras = 1.000000000000000e+00 -6.080940750293251e-17 -1.241272137099972e-17 +yras = 6.067273522716807e-17 9.999424457780739e-01 -1.072870595008693e-02 +zras = 1.306441321857720e-17 1.072870595008693e-02 9.999424457780740e-01 +cras = 4.763511657714858e+00 -1.033711122907699e+01 2.846804976463318e+01 +dst volume info +valid = 1 +filename = none +volume = 512 512 201 +voxelsize = 5.859379768371582e-01 5.859379768371582e-01 1.250000000000000e+00 +xras = -1.000000000000000e+00 0.000000000000000e+00 0.000000000000000e+00 +yras = 0.000000000000000e+00 1.000000000000000e+00 0.000000000000000e+00 +zras = 0.000000000000000e+00 0.000000000000000e+00 1.000000000000000e+00 +cras = -1.220703125000000e-04 5.858154296875000e-01 -2.437500000000000e+01 diff --git a/onlylian/mri_synthmorph/CMakeLists.txt b/onlylian/mri_synthmorph/CMakeLists.txt new file mode 100755 index 0000000..a1f55ee --- /dev/null +++ b/onlylian/mri_synthmorph/CMakeLists.txt @@ -0,0 +1,22 @@ +project(mri_synthmorph) + +file(GLOB WEIGHTS "synthmorph.*.h5") + +if (FSPYTHON_INSTALL_TREE) + install_pyscript_fspython_tree(mri_synthmorph) + install_symlinks_fspython_tree(TYPE files DESTINATION models ${WEIGHTS}) + install_directories_fspython_tree(synthmorph DESTINATION python/packages) +else() + install_pyscript(mri_synthmorph) + install_symlinks(TYPE files DESTINATION models ${WEIGHTS}) + install_directories(synthmorph DESTINATION python/packages) +endif() + +install_configured(fs-synthmorph-reg DESTINATION bin) + +## 08/2024 - currently failing on Intel Mac + +if(NOT APPLE) + add_test_script(NAME mri_synthmorph_test_register SCRIPT test_register.sh) + add_test_script(NAME mri_synthmorph_test_apply SCRIPT test_apply.sh) +endif() diff --git a/onlylian/mri_synthmorph/Dockerfile b/onlylian/mri_synthmorph/Dockerfile new file mode 100755 index 0000000..5961100 --- /dev/null +++ b/onlylian/mri_synthmorph/Dockerfile @@ -0,0 +1,46 @@ +# Define base image. Set HOME to avoid Matplotlib warning about non-writable +# MPLCONFIGDIR on Neurite import when running as non-root user. +FROM tensorflow/tensorflow:2.17.0-gpu AS base +ENV FREESURFER_HOME=/freesurfer +ENV PYTHONUSERBASE="$FREESURFER_HOME/env" +ENV PATH="$FREESURFER_HOME:$PATH" +ENV HOME=/tmp + + +# Intermediate build stage. Install Python packages to user base for easy COPY. +FROM base AS copy + +COPY --chmod=0775 mri_synthmorph $FREESURFER_HOME/ +COPY --chmod=0664 synthmorph/*.py $FREESURFER_HOME/synthmorph/ +COPY --chmod=0664 synthmorph.*.h5 $FREESURFER_HOME/models/ + +RUN apt-get update && apt-get install -y --no-install-recommends git +RUN python3 -m pip install -U pip +RUN python3 -m pip install --user \ + 'numpy<2.0' \ + git+https://github.com/adalca/pystrum.git@ba35d4b357f54e5ed577cbd413076a07ef810a21 \ + git+https://github.com/adalca/neurite.git@9ae2f5cec2201eedbcc6929cecf852193cef7646 \ + git+https://github.com/freesurfer/surfa.git@041905fca717447780e0cc211197669e3218de2f \ + git+https://github.com/voxelmorph/voxelmorph.git@53d1b95fa734648c92fd8af4f3807b09cb56c342 + +WORKDIR /artifacts +RUN python3 -V >python.txt +RUN python3 -m pip freeze >requirements.txt +RUN mri_synthmorph -h >help.general.txt +RUN mri_synthmorph register -h >help.register.txt +RUN mri_synthmorph apply -h >help.apply.txt + + +# Export Python requirements for reference. Build artifacts will only exist in +# in the target stage `export`. +FROM scratch AS export +COPY --from=copy /artifacts/*.txt / + + +# Exclude Git and caches from final image to save space. Copy only once to +# avoid unnecessary container layers. Set working directory to /mnt for data +# exchange with the host without having to specify the full path. +FROM base +COPY --from=copy $FREESURFER_HOME $FREESURFER_HOME +WORKDIR /mnt +ENTRYPOINT ["mri_synthmorph"] diff --git a/onlylian/mri_synthmorph/README.md b/onlylian/mri_synthmorph/README.md new file mode 100755 index 0000000..e88a279 --- /dev/null +++ b/onlylian/mri_synthmorph/README.md @@ -0,0 +1,106 @@ +# SynthMorph + +This guide explains how to build SynthMorph container images. +It assumes you execute commands in the `mri_synthmorph` directory. +For general information about SynthMorph, visit [synthmorph.io](https://synthmorph.io). + + +## Managing weight files with git-annex + +Weight files are large and therefore managed with `git-annex`. +Instructions with examples are available elsewhere: + +* https://surfer.nmr.mgh.harvard.edu/fswiki/GitAnnex +* https://git-annex.branchable.com/walkthrough + + +## Building SynthMorph images with Docker + +FreeSurfer automatically ships the most recent `mri_synthmorph` and weight files. +Building a standalone container image requires fetching and unlocking the model files with `git-annex`, replacing the symbolic links with the actual files. + +```sh +git fetch datasrc +git annex get . +git annex unlock synthmorph.*.h5 +``` + +Build a new image with the appropriate version tag: + +```sh +tag=X +docker build -t freesurfer/synthmorph:$tag . +``` + + +## Testing the local Docker image + +Update the version reference in the wrapper script and run it to test the local image with Docker. + +```sh +sed -i "s/^\(version = \).*/\1$tag/" synthmorph +./synthmorph -h +``` + + +## Testing with Apptainer + +Testing the image with Apptainer (Singularity) before making it public requires conversion. +If your home directory has a low quota, set up a cache elsewhere: + +```sh +d=$(mktemp -d) +export APPTAINER_CACHEDIR="$d" +export APPTAINER_TMPDIR="$d" +``` + +On the machine running Docker, convert the image with: + +```sh +apptainer build -f synthmorph_$tag.sif docker-daemon://freesurfer/synthmorph:$tag +``` + +If you want to test the image on another machine, save it first. +After transfer to the target machine, build a SIF file as a non-root user using the fakeroot feature. +This relies on namespace mappings set up in /etc/subuid and /etc/subgid (likely by Help). + +```sh +docker save synthmorph:$tag | gzip >synthmorph_$tag.tar.gz +apptainer build -f synthmorph_$tag.sif docker-archive://synthmorph_$tag.tar.gz +``` + +Finally, run the image. + +```sh +apptainer run --nv -e -B /autofs synthmorph_$tag.sif +``` + + +## Pushing to the Docker Hub + +Push the new image to the Docker Hub to make it public. +Update the default "latest" tag, so that `docker pull freesurfer/synthmorph` without a tag will fetch the most recent image. + +```sh +docker push freesurfer/synthmorph:$tag +docker tag freesurfer/synthmorph:$tag freesurfer/synthmorph:latest +docker push freesurfer/synthmorph:latest +``` + + +## Exporting Python requirements + +Export build artifacts for users who wish to create a custom Python environment. + +```sh +docker build --target export --output env . +``` + + +## Final steps + +Lock the annexed weight files again to prevent modification. + +```sh +git annex lock synthmorph.*.h5 +``` diff --git a/onlylian/mri_synthmorph/container-script b/onlylian/mri_synthmorph/container-script new file mode 100755 index 0000000..58a7dc2 --- /dev/null +++ b/onlylian/mri_synthmorph/container-script @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +# This wrapper script facilitates setup and use of SynthMorph containers by +# pulling them from the Docker Hub and mounting the host directory defined by +# environment variable SUBJECTS_DIR to /mnt in the container. Invoke the script +# just like `mri_synthmorph` in FreeSurfer, with one exception: you can only +# read and write data under SUBJECTS_DIR, which will be the working directory +# in the container. If unset, SUBJECTS_DIR defaults to your current directory. +# This means you can access relative paths under your working directory without +# setting SUBJECTS_DIR. In other words, SUBJECTS_DIR sets the working directory +# for SynthMorph, and you can specify paths relative to it. + +# Update the version to pull a different image, unless you already have it. +version = 4 + +# Local image location for Apptainer/Singularity. Set an absolute path to avoid +# pulling new images when you change the folder. Ignored for Docker and Podman. +sif_file = f'synthmorph_{version}.sif' + +# We will use the first of the below container systems found in your PATH, from +# left to right. You may wish to reorder them, If you have several installed. +tools = ('docker', 'apptainer', 'singularity', 'podman') + + +import os +import sys +import signal +import shutil +import subprocess + + +# Report version. Avoid errors when piping, for example to `head`. +signal.signal(signal.SIGPIPE, handler=signal.SIG_DFL) +hub = 'https://hub.docker.com/u/freesurfer' +print(f'Running SynthMorph version {version} from {hub}') + + +# Find a container system. +for tool in tools: + path = shutil.which(tool) + if path: + print(f'Using {path} to manage containers') + break + +if not path: + print(f'Cannot find container tools {tools} in PATH', file=sys.stderr) + exit(1) + + +# Prepare bind path and URL. Mount SUBJECTS_DIR as /mnt inside the container, +# which we made the working directory when building the image. While Docker +# and Podman will respect it, they require absolute paths for bind mounts. +host = os.environ.get('SUBJECTS_DIR', os.getcwd()) +host = os.path.abspath(host) +print(f'Will bind /mnt in image to SUBJECTS_DIR="{host}"') + +image = f'freesurfer/synthmorph:{version}' +if tool != 'docker': + image = f'docker://{image}' + + +# Run Docker containers with the UID and GID of the host user. This user will +# own bind mounts inside the container, preventing output files owned by root. +# Root inside a rootless Podman container maps to the non-root host user, which +# is what we want. If we set UID and GID inside the container to the non-root +# host user as we do for Docker, then these would get remapped according to +# /etc/subuid outside, causing problems with read and write permissions. +if tool in ('docker', 'podman'): + arg = ('run', '--rm', '-v', f'{host}:/mnt') + + # Pretty-print help text. + if sys.stdout.isatty(): + arg = (*arg, '-t') + if tool == 'docker': + arg = (*arg, '-u', f'{os.getuid()}:{os.getgid()}') + + arg = (*arg, image) + + +# For Apptainer/Singularity, the user inside and outside the container is the +# same. The working directory is also the same, unless we explicitly set it. +if tool in ('apptainer', 'singularity'): + arg = ('run', '--nv', '--pwd', '/mnt', '-e', '-B', f'{host}:/mnt', sif_file) + + if not os.path.isfile(sif_file): + print(f'Cannot find image {sif_file}, pulling it', file=sys.stderr) + proc = subprocess.run((tool, 'pull', sif_file, image)) + if proc.returncode: + exit(proc.returncode) + + +# Summarize and launch container. +print('Command:', ' '.join((tool, *arg))) +print('SynthMorph arguments:', *sys.argv[1:]) +proc = subprocess.run((tool, *arg, *sys.argv[1:])) +exit(proc.returncode) diff --git a/onlylian/mri_synthmorph/fs-synthmorph-reg b/onlylian/mri_synthmorph/fs-synthmorph-reg new file mode 100755 index 0000000..ab9a533 --- /dev/null +++ b/onlylian/mri_synthmorph/fs-synthmorph-reg @@ -0,0 +1,1001 @@ +#!/bin/tcsh -f +# fs-synthmorph-reg - sources +if(-e $FREESURFER_HOME/sources.csh) then + source $FREESURFER_HOME/sources.csh +endif + +if($?FSMNI152DIR == 0) setenv FSMNI152DIR $FREESURFER/average/mni_icbm152_nlin_asym_09c +unsetenv FS_LOCAL_PYTHONPATH + +set VERSION = '$Id$'; +set scriptname = `basename $0` + +set outdir = (); +set subject = (); +set m3z = (); +set m3zinv = () +set invol = () +set targvol = () +set lta2 = () +set vgthresh = 1e-5 +set StripInput = 0 +set StripTarget = 0 +set ReRun = 0 + +set threads = 1 +set ForceUpdate = 0 +set antsreg = antsRegistrationSyNQuick.sh +set UseQuick = 0 +set CropInput = 1 +set CropTarget = 1 +set MNITarget = 1 +# Synthmorph uses 1mm internally, so useing target res=1mm does not +# slow things down or take more mem and it is probably gives a little +# bit better registration. ANTs speed and performance will be +# affected. +set MNITargetRes = 1.0mm +set MNIOutputRes = 1.0mm +set DoCBIG = 0 +set DoTest = 0 +set dim = 3 +set UseAnts = 0 # 0 means use synthmorph +set ComputeInverse = 1 +set PitStr = "" +set AffineOnly = 0 + +set tmpdir = (); +set cleanup = 1; +set LF = (); + +set inputargs = ($argv); +set PrintHelp = 0; +if($#argv == 0) goto usage_exit; +set n = `echo $argv | grep -e -help | wc -l` +if($n != 0) then + set PrintHelp = 1; + goto usage_exit; +endif +set n = `echo $argv | grep -e -version | wc -l` +if($n != 0) then + echo $VERSION + exit 0; +endif +goto parse_args; +parse_args_return: +goto check_params; +check_params_return: + +set StartTime = `date`; +set tSecStart = `date '+%s'`; +set year = `date +%Y` +set month = `date +%m` +set day = `date +%d` +set hour = `date +%H` +set min = `date +%M` + +if($#outdir) then + mkdir -p $outdir/log +else + set outdir = `dirname $m3z` + mkdir -p $outdir +endif +pushd $outdir > /dev/null +set outdir = `pwd`; +popd > /dev/null + +if($#tmpdir == 0) set tmpdir = $outdir/tmp +mkdir -p $tmpdir + +# Set up log file +if($#LF == 0) set LF = $outdir/log/fs-synthmorph-reg.Y$year.M$month.D$day.H$hour.M$min.log +if($LF != /dev/null) rm -f $LF +echo "Log file for fs-synthmorph-reg" >> $LF +date | tee -a $LF +echo "" | tee -a $LF +echo "setenv SUBJECTS_DIR $SUBJECTS_DIR" | tee -a $LF +echo "cd `pwd`" | tee -a $LF +echo $0 $inputargs | tee -a $LF +ls -l $0 | tee -a $LF +echo "" | tee -a $LF +cat $FREESURFER_HOME/build-stamp.txt | tee -a $LF +echo $VERSION | tee -a $LF +uname -a | tee -a $LF +echo "pid $$" | tee -a $LF +if($?PBS_JOBID) then + echo "pbsjob $PBS_JOBID" >> $LF +endif +if($?SLURM_JOB_ID) then + echo SLURM_JOB_ID $SLURM_JOB_ID >> $LF +endif + +#======================================================== +# Note: Ants might not be deterministc with threads +setenv OMP_NUM_THREADS $threads +setenv ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS $threads + +if($StripInput) then + #set involstripped = $tmpdir/invol.stripped.nii.gz # breaks header + set involstripped = $tmpdir/invol.stripped.mgz + set ud = `UpdateNeeded $involstripped $invol` + if($ud || $ForceUpdate) then + set cmd = (mri_synthstrip -i $invol -o $involstripped -t $threads) + echo $cmd | tee -a $LF + $cmd | tee -a $LF + if($status) exit 1 + endif + set invol = $involstripped +endif +if($StripTarget) then + #set targvolstripped = $tmpdir/targ.stripped.nii.gz # breaks header + set targvolstripped = $tmpdir/targ.stripped.mgz + set ud = `UpdateNeeded $targvolstripped $targvol` + if($ud || $ForceUpdate) then + set cmd = (mri_synthstrip -i $targvol -o $targvolstripped -t $threads) + echo $cmd | tee -a $LF + $cmd | tee -a $LF + if($status) exit 1 + endif + set targvol = $targvolstripped +endif + +if($CropInput) then # Crop for speed + # Input volume + set involcrop = $tmpdir/invol.crop.nii.gz + set ud = `UpdateNeeded $involcrop $invol` + if($ud || $ForceUpdate) then + set cmd = (mri_mask -bb 3 $invol $invol $involcrop) + echo $cmd | tee -a $LF + $cmd | tee -a $LF + if($status) exit 1 + endif + # To get back to the full FoV + set regcroptoinvol = $tmpdir/reg.crop-to-invol.lta + set ud = `UpdateNeeded $regcroptoinvol $involcrop $invol` + if($ud || $ForceUpdate) then + set cmd = (lta_convert --inlta identity.nofile --src $involcrop \ + --trg $invol --outlta $regcroptoinvol) + echo $cmd | tee -a $LF + $cmd |& tee -a $LF + if($status) exit 1 + endif +else + set involcrop = $invol + set regcroptoinvol = () +endif + +if($CropTarget) then + if(! $MNITarget) then + # Target vol + set targvolcrop = $tmpdir/targvol.crop.nii.gz + set ud = `UpdateNeeded $targvolcrop $targvol` + if($ud || $ForceUpdate) then + set cmd = (mri_mask -crop 3 $targvol $targvol $targvolcrop) + echo $cmd | tee -a $LF + $cmd | tee -a $LF + if($status) exit 1 + endif + # To get back to the full FoV + set regcroptotarg = $tmpdir/reg.crop-to-targ.lta + set ud = `UpdateNeeded $regcroptotarg $targvolcrop $targvol` + if($ud || $ForceUpdate) then + set cmd = (lta_convert --inlta identity.nofile --src $targvolcrop \ + --trg $targvol --outlta $regcroptotarg) + echo $cmd | tee -a $LF + $cmd |& tee -a $LF + if($status) exit 1 + endif + else + set targvolcrop = $FSMNI152DIR/reg-targets/mni152.${MNITargetRes}$PitStr.cropped.nii.gz + set regcroptotarg = $FSMNI152DIR/reg-targets/reg.${MNITargetRes}.cropped.to.${MNIOutputRes}.lta + # Note that lta does not have PitStr + endif +else + set targvolcrop = $targvol + set regcroptotarg = () +endif + +if($UseAnts) then +# Compute both the affine and the warp with ANTs. The Affine goes from +# the input croped space to the target cropped space. The warp goes +# from target cropped to target cropped. +set warp = $tmpdir/reg.1Warp.nii.gz +set affinemat = $tmpdir/reg.0GenericAffine.mat +set ud = `UpdateNeeded $warp $involcrop $targvol` +if($ud || $ForceUpdate) then + date | tee -a $LF + pushd $tmpdir # this program can crash if stuff in the current dir + if(! $DoCBIG) then + set cmd = ($antsreg -d $dim -m $involcrop -f $targvolcrop -o reg. -n $threads) + else + set cmd = (antsRegistration --dimensionality $dim \ + --output [reg.,reg.Warped.nii.gz,reg.InverseWarped.nii.gz] \ + --initial-moving-transform [ $involcrop, $targvolcrop, 1 ] \ + --collapse-output-transforms 1 \ + --use-histogram-matching 1 \ + --use-estimate-learning-rate-once 1 \ + --metric mattes[ $fixed, $moving, 1, 32, regular, 0.3] \ + --transform affine[ 0.1 ] \ + --convergence [ 100x100x200, 1.e-8, 20 ] \ + --smoothing-sigmas 4x2x1vox \ + --shrink-factors 3x2x1 -l 1 \ + -metric cc[ $fixed, $moving, 1, 4] \ + --transform SyN[ .20, 3, 0] \ + --convergence [ 100x100x50, 0, 5 ] \ + --smoothing-sigmas 1x0.5x0vox \ + --shrink-factors 4x2x1) + endif + echo "\n\n"| tee -a $LF + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF + if($CropInput || $CropTarget) then + # Somehow ANTs will change the geom slightly, so map it back to the + # the target space. This is a bit of a hack that seems to work, but + # it is not principled (ie, it was derived by trial and error, so + # there is a danger that it will not work all the time). + set cmd = (mri_vol2vol --regheader --targ $targvolcrop --mov $warp --o $warp) + echo "\n\n"| tee -a $LF + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF + endif + popd +endif + +# Convert the ANTs affine to txt file (maps involcrop to the input of the targvolcrop space) +set affinetxt = $tmpdir/reg.0GenericAffine.txt +set ud = `UpdateNeeded $affinetxt $affinemat` +if($ud || $ForceUpdate) then + date | tee -a $LF + set cmd = (ConvertTransformFile $dim $affinemat $affinetxt --hm --ras) + echo "\n\n"| tee -a $LF + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF +endif + +# Convert the ANTs affine to an LTA +set affinelta = $tmpdir/reg.0GenericAffine.lta +set ud = `UpdateNeeded $affinelta $affinetxt` +if($ud || $ForceUpdate) then + date | tee -a $LF + set cmd = (lta_convert --src $involcrop --trg $targvolcrop --outlta $affinelta) + if($dim == 2) set cmd = ($cmd --inniftyreg2d $affinetxt); + if($dim == 3) set cmd = ($cmd --inniftyreg $affinetxt); + echo "\n\n"| tee -a $LF + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF +endif + +# Create one lta that maps from the uncropped input vol space to the taret space +if($CropInput || $CropTarget) then + set regcroptargtocropinvol = $tmpdir/reg.croptarg_to_cropinvol.lta + set ud = `UpdateNeeded $regcroptargtocropinvol $affinelta $regcroptoinvol` + if($ud || $ForceUpdate) then + date | tee -a $LF + set cmd = (mri_concatenate_lta -invert1 -invertout $affinelta $regcroptoinvol $regcroptargtocropinvol) + echo "\n\n"| tee -a $LF + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF + endif +else + set regcroptargtocropinvol = $affinelta +endif + +# Concatenate the affines and the warp (ANTS) +set ud = `UpdateNeeded $m3z $warp $targvolcrop $regcroptargtocropinvol $regcroptotarg ` +if($ud || $ForceUpdate) then + date | tee -a $LF + set cmd = (mri_warp_convert --initk $warp --insrcgeom $targvolcrop \ + --outm3z $m3z --vg-thresh $vgthresh) + if($CropInput) set cmd = ($cmd --lta1-inv $regcroptargtocropinvol ) + if($#regcroptotarg) set cmd = ($cmd --lta2 $regcroptotarg) + echo "\n\n"| tee -a $LF + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF +endif + +else # End ANTS ================= else Use synthmorph + +set gpuopt = "" +# Compute both the affine, maps from involcrop to targvolcrop +set affinelta = $outdir/aff.lta +set ud = `UpdateNeeded $affinelta $involcrop $targvolcrop` +if($ud || $ForceUpdate) then + date | tee -a $LF + set cmd = (mri_synthmorph -m affine -t $affinelta $involcrop $targvolcrop -j $threads $gpuopt) + echo "\n\n"| tee -a $LF + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit +endif + +# Create one lta that maps from the uncropped input vol space to the (cropped) target space +set regtargtoinvol = $outdir/reg.targ_to_invol.lta +set reginvoltotarg = $outdir/reg.invol_to_targ.lta +if($CropInput || $CropTarget) then + set reginvoltocroptarg = $tmpdir/reg.invol_to_croptarg.lta + set ud = `UpdateNeeded $reginvoltocroptarg $affinelta $regcroptoinvol` + if($ud || $ForceUpdate) then + date | tee -a $LF + set cmd = (mri_concatenate_lta -invert1 -invertout $affinelta $regcroptoinvol $reginvoltocroptarg) + echo "\n\n"| tee -a $LF + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF + endif + if($MNITarget) then + # Create one lta that maps from the uncropped input vol space to the uncropped target space + # This produces a linear tranform that can be used to map from target to input volume just + # like the nonlinear transform + set ud = `UpdateNeeded $regtargtoinvol $reginvoltocroptarg` + if($ud || $ForceUpdate) then + set regtargtocropped = $FSMNI152DIR/reg-targets/reg.$MNIOutputRes.to.$MNITargetRes.cropped.lta + set cmd = (mri_concatenate_lta -invert2 $regtargtocropped $reginvoltocroptarg $regtargtoinvol) + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF + endif + endif + # Compute the inverse: invol-to-targ + set ud = `UpdateNeeded $reginvoltotarg $regtargtoinvol` + if($ud || $ForceUpdate) then + set cmd = (lta_convert --invert --inlta $regtargtoinvol --outlta $reginvoltotarg) + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF + endif +else + pushd $tmpdir + ln -fs aff.lta reg.invol_to_targ.lta + popd + set reginvoltocroptarg = $affinelta + set reginvoltotarg = $affinelta + # Compute target to invol + set ud = `UpdateNeeded $regtargtoinvol $reginvoltotarg` + if($ud || $ForceUpdate) then + set cmd = (lta_convert --invert --inlta $reginvoltotarg --outlta $regtargtoinvol) + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF + endif + +endif + +if($MNITarget) then + # Create an xfm file which can be used as the taliarch.xfm + set xfm = $outdir/reg.invol_to_targ.xfm + set ud = `UpdateNeeded $xfm $reginvoltotarg` + if($ud) then + set cmd = (lta_convert --inlta $reginvoltotarg --outmni $xfm) + echo $cmd | tee -a $LF + $cmd |& tee -a $LF + if($status) goto error_exit + echo "\n\n"| tee -a $LF + endif +endif + +echo "" | tee -a $LF +echo "To check affine registration" | tee -a $LF +echo "tkregisterfv --mov $invol --targ $targvol --reg $reginvoltotarg" | tee -a $LF +echo "" | tee -a $LF + +if($AffineOnly) then + echo "AffineOnly specified, so exiting now" | tee -a $LF + goto done +endif + +# Now compute the nonlinear part +set deform = $tmpdir/deform.mgz +set smvol = $tmpdir/synthmorph.out.mgz +set ud = `UpdateNeeded $deform $affinelta $involcrop $targvolcrop` +if($ud) then + set cmd = (mri_synthmorph -m deform -t $deform -i $affinelta $involcrop $targvolcrop -j $threads $gpuopt) + if($DoTest) set cmd = ($cmd -o $smvol); # can be a pain when you want to rerun to create test output + echo "\n\n" | tee -a $LF + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit +endif + +# Compute warp to the full FoV of the input (synthmorph) +set ud = `UpdateNeeded $m3z $deform $targvolcrop $reginvoltocroptarg $regcroptotarg` +if($ud || $ForceUpdate) then + # This call is not intuitive to me. It appears that the insrcgeom + # will set the vox2ras for the image-side of the warp. The warp + # maps from target voxel to cropped input RAS. The insrcgeom + # indicates how to go from that RAS to a CRS in the cropped input, + # then the lta1 indicates how to go from the cropped CRS to the + # uncropped CRS. + set cmd = (mri_warp_convert --inras $deform --insrcgeom $involcrop\ + --outm3z $m3z --vg-thresh $vgthresh) + if($CropInput) set cmd = ($cmd --lta1-inv $regcroptoinvol) + if($#regcroptotarg) set cmd = ($cmd --lta2 $regcroptotarg) + echo "\n\n$cmd" | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit +endif + +endif # Synthmorph + +if($ComputeInverse) then + set ud = `UpdateNeeded $m3zinv $m3z` + if($ud || $ForceUpdate) then + set cmd = (mri_ca_register -invert-and-save $m3z $m3zinv) + echo "\n\n$cmd" | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) goto error_exit + endif +endif + +# Test against output generated by the registration program +if($DoTest) then + if($UseAnts) then + set mvol = $tmpdir/reg.Warped.nii.gz + else + set mvol = $smvol + endif + if($CropInput || $CropTarget) then + # Ants will output to the cropped target space, need to resample + # to the full uncropped space. + set mvol2 = $tmpdir/morph.out.nii.gz + set ud = `UpdateNeeded $mvol2 $mvol $targvol` + if($ud || $ForceUpdate) then + set cmd = (mri_vol2vol --regheader --mov $mvol --targ $targvol --o $mvol2) + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) exit 1 + endif + set mvol = $mvol2 + endif + + set ud = `UpdateNeeded $testvol $invol $m3z $mvol` + if($ud || $ForceUpdate) then + set cmd = (mri_convert -rt nearest $invol -at $m3z $testvol) + echo $cmd | tee -a $LF + fs_time $cmd |& tee -a $LF + if($status) exit 1 + set cmd = (mri_diff --po $testvol $mvol) + echo $cmd | tee -a $LF + $cmd | tee $tmpdir/test.diff.dat |& tee -a $LF + endif + echo "tkmeditfv -f $targvol -aux $testvol" |& tee -a $LF +endif + +#======================================================== + +done: + +# Cleanup +if($cleanup) rm -rf $tmpdir + +# Done +echo " " |& tee -a $LF +set tSecEnd = `date '+%s'`; +@ tSecRun = $tSecEnd - $tSecStart; +set tRunMin = `echo $tSecRun/60|bc -l` +set tRunMin = `printf %5.2f $tRunMin` +set tRunHours = `echo $tSecRun/3600|bc -l` +set tRunHours = `printf %5.2f $tRunHours` +echo "Started at $StartTime " |& tee -a $LF +echo "Ended at `date`" |& tee -a $LF +echo "Fs-Synthmorph-Reg-Run-Time-Sec $tSecRun" |& tee -a $LF +echo "Fs-Synthmorph-Reg-Run-Time-Min $tRunMin" |& tee -a $LF +echo "Fs-Synthmorph-Reg-Run-Time-Hours $tRunHours" |& tee -a $LF +echo " " |& tee -a $LF +echo "fs-synthmorph-reg Done" |& tee -a $LF +exit 0 + +############################################### + +############--------------################## +error_exit: +echo "ERROR:" + +exit 1; +############################################### + +############--------------################## +parse_args: +set cmdline = ($argv); +while( $#argv != 0 ) + + set flag = $argv[1]; shift; + + switch($flag) + + case "--i": + if($#argv < 1) goto arg1err; + set invol = $argv[1]; shift; + if(! -e $invol) then + echo "ERROR: cannot find $invol" + exit 1 + endif + set invol = `getfullpath $invol` + breaksw + + case "--t": + if($#argv < 1) goto arg1err; + set targvol = $argv[1]; shift; + if(! -e $targvol) then + echo "ERROR: cannot find $targvol" + exit 1 + endif + set targvol = `getfullpath $targvol` + set MNITarget = 0 + set Crop = 1 + breaksw + + case "--warp": + case "--m3z": + if($#argv < 1) goto arg1err; + set m3z = $argv[1]; shift; + breaksw + + case "--affine-only": + set AffineOnly = 1 + breaksw + + case "--o": + if($#argv < 1) goto arg1err; + set outdir = $argv[1]; shift; + breaksw + + case "--s": + if($#argv < 1) goto arg1err; + set subject = $argv[1]; shift; + breaksw + + case "--sd": + if($#argv < 1) goto arg1err; + setenv SUBJECTS_DIR $argv[1]; shift; + breaksw + + case "--threads": + if($#argv < 1) goto arg1err; + set threads = $argv[1]; shift; + breaksw + + case "--vg-thresh": + if($#argv < 1) goto arg1err; + set vgthresh = $argv[1]; shift; + breaksw + + case "--synthmorph": + set UseAnts = 0 + breaksw + case "--ants": + set UseAnts = 1 + breaksw + + case "--test": + set DoTest = 1 + breaksw + case "--no-test": + set DoTest = 0 + breaksw + + case "--2d": + set dim = 2 + set DoTest = 0 + breaksw + + case "--quick": + set antsreg = antsRegistrationSyNQuick.sh + set UseQuick = 1 + breaksw + case "--no-quick": + case "--syn": + set antsreg = antsRegistrationSyN.sh + set UseQuick = 0 + breaksw + + case "--crop": + set CropInput = 1 + set CropTarget = 1 + breaksw + case "--no-crop": + set CropInput = 0 + set CropTarget = 0 + breaksw + + case "--strip": + set StripInput = 1 + set StripTarget = 1 + breaksw + case "--no-strip": + set StripInput = 0 + set StripTarget = 0 + breaksw + case "--strip-input": + set StripInput = 1 + breaksw + case "--no-strip-input": + set StripInput = 0 + breaksw + case "--strip-target": + set StripTarget = 1 + breaksw + case "--no-strip-target": + set StripTarget = 0 + breaksw + + case "--inv": + set ComputeInverse = 1 + breaksw + case "--no-inv": + set ComputeInverse = 0 + breaksw + + case "--mni": + set MNITarget = 1 + breaksw + case "--no-nmni": + set MNITarget = 0 + breaksw + + case "--mni-res": + case "--mni-int-res": + case "--mni-targ-res": + if($#argv < 1) goto arg1err; + set MNITargetRes = $argv[1]; shift; + if($MNITargetRes != 1.0mm && $MNITargetRes != 1.5mm && $MNITargetRes != 2.0mm) then + echo "ERROR: --mni-res must be 1.0mm, 1.5mm, or 2.0mm" + exit 1 + endif + set MNITarget = 1 + breaksw + + case "--mni-output-res": + case "--mni-out-res": + if($#argv < 1) goto arg1err; + set MNIOutputRes = $argv[1]; shift; + if($MNIOutputRes != 1.0mm && $MNIOutputRes != 1.5mm && $MNIOutputRes != 2.0mm && $MNIOutputRes != conformed) then + echo "ERROR: --mni-out-res must be 1.0mm, 1.5mm, 2.0mm, or conformed" + exit 1 + endif + set MNITarget = 1 + breaksw + + case "--mni-1": + set MNIOutputRes = 1.0mm + set MNITarget = 1 + breaksw + + case "--mni-1.5": + set MNIOutputRes = 1.5mm + set MNITarget = 1 + breaksw + + case "--mni-2": + set MNIOutputRes = 2.0mm + set MNITarget = 1 + breaksw + + case "--mni-conformed": + set MNIOutputRes = conformed + set MNITarget = 1 + breaksw + + case "--pituitary": + set PitStr = ".pit" + breaksw + + case "--cbig": + set DoCBig = 1 + breaksw + case "--no-cbig": + set DoCBig = 0 + breaksw + + case "--force": + set ForceUpdate = 1 + breaksw + case "--rerun": + # Prevents it from bailing out on the first check. This can be + # handy when debugging and you want it to get into the interior + # of the code. + set ReRun = 1 + breaksw + + case "--log": + if($#argv < 1) goto arg1err; + set LF = $argv[1]; shift; + breaksw + + case "--nolog": + case "--no-log": + set LF = /dev/null + breaksw + + case "--tmp": + case "--tmpdir": + if($#argv < 1) goto arg1err; + set tmpdir = $argv[1]; shift; + set cleanup = 0; + breaksw + + case "--nocleanup": + set cleanup = 0; + breaksw + + case "--cleanup": + set cleanup = 1; + breaksw + + case "--debug": + set verbose = 1; + set echo = 1; + breaksw + + default: + echo ERROR: Flag $flag unrecognized. + echo $cmdline + exit 1 + breaksw + endsw + +end + +goto parse_args_return; +############--------------################## + +############--------------################## +check_params: + +if($UseAnts == -1) then + echo "ERROR: must spec either --ants or --synthmorph" + exit 1 +endif + +# This will keep the temp stuff from being deleted +if($#outdir && $#tmpdir == 0) then + set tmpdir = $outdir + set cleanup = 0 +endif + +if($#subject) then + set sd = $SUBJECTS_DIR/$subject + if(! -e $sd) then + echo "ERROR: cannot find $subject" + exit 1; + endif + if($#invol == 0) set invol = $sd/mri/norm.mgz + if($#outdir == 0 && $MNITarget) then + if($UseAnts) then + set outdir = $sd/mri/transforms/ants.warp.$MNITargetRes.$MNIOutputRes$PitStr + else + set outdir = $sd/mri/transforms/synthmorph.$MNITargetRes.$MNIOutputRes$PitStr + endif + #don't create particular files in the transform dir + #set m3z = $outdir.nii.gz + #set m3zinv = $outdir.inv.nii.gz # Just in case the invert is requested + endif +endif + +# This is too complicated now as it is not just setting the target volume to +# the cropped; have to set the reg-crop-to-target too +#if($MNITarget) set CropTarget = 0; + +if(! $MNITarget) then + # Could get this to work, but not really needed + set CropTarget = 0; + set CropInput = 0; +endif + +if($#outdir == 0) then + echo "ERROR: must spec output dir" + exit 1 +endif +if($#invol == 0) then + echo "ERROR: must spec input volume" + exit 1 +endif +foreach f ($invol $targvol) + if(! -e $f) then + echo "ERROR: cannot find $f" + exit 1 + endif +end + +mkdir -p $outdir +set outdir = `getfullpath $outdir` +if($#m3z == 0) then + if($MNITarget) then + # Lose ants vs synthseg distinction in the output + set m3z = $outdir/warp.to.mni152.${MNITargetRes}.${MNIOutputRes}.nii.gz + set m3zinv = $outdir/warp.to.mni152.${MNITargetRes}.${MNIOutputRes}.inv.nii.gz + else + set m3z = $outdir/warp.nii.gz + set m3zinv = $outdir/warp.inv.nii.gz + endif +endif + +if($ComputeInverse && $#m3zinv == 0) then + set stem = `fname2stem $m3z` + set ext = `fname2ext $m3z` + set m3zinv = $stem.inv.$ext +endif + +if($#targvol && $MNITarget) then + echo "ERROR: cannot specify both target volume and mni target" + exit 1 +endif + +# MNI is already skull stripped +if($MNITarget) set targvol = $FSMNI152DIR/reg-targets/mni152.${MNIOutputRes}${PitStr}.nii.gz + +if($UseQuick) then + set antsreg = antsRegistrationSyNQuick.sh +else + set antsreg = antsRegistrationSyN.sh +endif + +set testvol = () +if($DoTest) set testvol = $outdir/test.nii.gz + +set ud1 = `UpdateNeeded $m3z $targvol $invol` +set ud2 = 0 +if($ComputeInverse) set ud2 = `UpdateNeeded $m3zinv $targvol $invol` +if($ReRun == 0 && $ud1 == 0 && $ud2 == 0 && $ForceUpdate == 0) then + echo "fs-synthmorph-reg: update not needed" + exit 0 +endif + +goto check_params_return; +############--------------################## + +############--------------################## +arg1err: + echo "ERROR: flag $flag requires one argument" + exit 1 +############--------------################## +arg2err: + echo "ERROR: flag $flag requires two arguments" + exit 1 +############--------------################## + +############--------------################## +usage_exit: + echo "" + echo "fs-synthmorph-reg -- frontend for running mri_synthmorph (24GB)" + echo " --i invol" + echo " --t targetvol" + echo " --warp warp.{m3z,mgz,nii.gz} : output warp file (or save in outdir)" + echo " --o outdir" + echo " --s subject (invol=norm.mgz, warp=mri/transforms/synthmorph.1.0mm.1.0mm.nii.gz targ=MNI152.1mm)" + echo " --threads threads" + echo " --mni-targ-res 1.0mm, 1.5mm, 2.0mm (default is 1.0mm)" + echo " --mni-out-res 1.0mm, 1.5mm, 2.0mm, conformed (default is 1.0mm)" + echo " --no-inv : do not compute warp inverse (computed by default)" + echo " --affine-only : only perform the affine registration part" + echo " --strip : skull strip input and target" + echo " --strip-input : skull strip input" + echo " --strip-target : skull strip target" + echo " --crop/--no-crop (default is $CropInput, but no-crop if not registering to mni)" + echo " --pituitary : select the MNI target volume that does not mask out the pituitary" + echo " --test : resample the input volume to the output space and store as test.nii.gz" + echo " --vg-thresh vgthresh : threshold for testing diffs in volume geom" + echo " --ants : use ANTs registration intstead of synthmorph" + echo " --force : regenerate all output " + echo " --rerun : for debugging only, when you want it to get into the inteiror of the code" + echo " --tmp tmpdir" + echo " " + + + if(! $PrintHelp) exit 1; + echo $VERSION + cat $0 | awk 'BEGIN{prt=0}{if(prt) print $0; if($1 == "BEGINHELP") prt = 1 }' +exit 1; + +#---- Everything below here is printed out as part of help -----# +BEGINHELP + +This is a frontend for running mri_synthmorph, especially for +registering to mni152 space. For typical usage, it will consume less +than 15GB of memory. For affine only it requires less than 7GB. + +Simple usage: + +fs-synthmorph-reg --i inputvol.nii.gz --o synthmorphdir + +This will register to the mni152 saving output files in synthmorphdir. The nonlinear +warp will be outdir/warp.to.mni152.1.0mm.1.0mm.nii.gz which can be applied like: + +mri_convert inputvol.nii.gz -at warp.to.mni152.1.0mm.1.0mm.nii.gz inputvol.mni152.nii.gz + +which can be checked with + +freeview inputvol.mni152.nii.gz $FREESURFER/average/mni_icbm152_nlin_asym_09c/reg-targets/mni152.1.0mm.nii.gz + +If you have created the warp from a FreeSurfer subject space (either with --s or passing a +conformed volume), then you can apply the warp to the surfaces, eg, + +mris_apply_reg --warp lh.white warp.to.mni152.1.0mm.1.0mm.inv.nii.gz lh.warp.mni152 + +which you can check with + +freeview inputvol.mni152.nii.gz $FREESURFER/average/mni_icbm152_nlin_asym_09c/reg-targets/mni152.1.0mm.nii.gz -f lh.warp.mni152 + +Skull stripping: mri_synthmorph is quite robust to the presence or +absence of skull stripping, but there will be some differences, so, to +be on the safe side, we recommend that images be skull stripped prior +to registration. When registering to the mni152, it will automatically +use a skull stripped volume. If you want fs-synthmorph-reg to skull +strip for you, then add --strip (uses mri_synthstrip). Note that if +you pass a FS subject (--s), it will use the norm.mgz volume, which is +already skull stripped. Note that ANTs performance and speed will be +very sensitive to stripping. + +Cropping: internally, fs-synthmorph-reg will crop the input to a +minimal window around non-zero voxels in the image. In the end, this +probably does not have much of an effect for synthmorph as it will +sample to 256^3 internally. For ANTs, this can speed up the program +dramatically and reduce its memory footprint. fs-synthmorph-reg will +manage all of the transforms so that they all reference the uncropped +space, ie, the user is completely insulated from the cropping process +and its implications. To turn off cropping, use --no-crop. If you are +not stripping, it probably does not make sense to crop. + +Target and Output MNI Resolution: the first 1.0mm in the warp file +name means that the registration is done to a version of the mni152 +with an isotropic resolution of 1.0mm (this will be how finely the +warp field is sampled). The second 1.0mm means that the warp file will +actually warp to the the mni152 with an isotropic resolution of 1.0mm. +You can change the resolution of the warp field with the +--mni-targ-res flag (options are 1.0mm, 1.5mm, 2.0mm), but, +internally, synthmorph uses 1mm, so this makes sense. You can change +the resolution of the output in several ways. From this program, you +can specify the output resolution with --mni-out-res (options are +1.0mm, 1.5mm, 2.0mm). You can also create an LTA file to the desired +output resolution and then use mri_warp_convert to change the warp. +Or you can create an LTA file to the desired resolution, and then use +mri_vol2vol --gcam to apply the warp and the LTA (instead of +mri_convert, which will only the the warp). + +Affine and affine-only: by default, this script will perform a +non-linear registration to the target space. The first step of this +will be to perform an affine registration. Sometimes, this is useful +too. This affine registration is stored in reg.targ_to_invol.lta. If +you ONLY want the affine registration, then you can specify +--affine-only. + +Arbitrary Targets: you can use fs-synthmorph-reg to register to target +volumes other than the mni152. To do this, simply specify --t +targetvol. You can have fs-synthmorph-reg skull strip the target by +adding --strip-target. By default, the target will be cropped unless +--no-crop (see Cropping above). + +Please cite SynthMorph: +Anatomy-specific acquisition-agnostic affine registration learned from +fictitious images +Hoffmann M, Hoopes A, Fischl B*, Dalca AV* (*equal contribution) +SPIE Medical Imaging: Image Processing, 12464, p 1246402, 2023 +https://doi.org/10.1117/12.2653251 +https://synthmorph.io/#papers (PDF) + +SynthMorph: learning contrast-invariant registration without acquired images +Hoffmann M, Billot B, Greve DN, Iglesias JE, Fischl B, Dalca AV +IEEE Transactions on Medical Imaging, 41 (3), 543-558, 2022 +https://doi.org/10.1109/TMI.2021.3116879 + +If you use SynthStrip in your analysis, please cite: +SynthStrip: Skull-Stripping for Any Brain Image +A Hoopes, JS Mora, AV Dalca, B Fischl, M Hoffmann +NeuroImage 206 (2022), 119474 +https://doi.org/10.1016/j.neuroimage.2022.119474 +Website: https://synthstrip.io + diff --git a/onlylian/mri_synthmorph/mri_synthmorph b/onlylian/mri_synthmorph/mri_synthmorph new file mode 100755 index 0000000..d965d85 --- /dev/null +++ b/onlylian/mri_synthmorph/mri_synthmorph @@ -0,0 +1,420 @@ +#!/usr/bin/env python3 + +import os +import sys +import pathlib +import argparse +import textwrap +import surfa as sf +from synthmorph import utils + + +# Argument settings. +default = { + 'model': 'joint', + 'hyper': 0.5, + 'extent': 256, + 'steps': 7, + 'method': 'linear', + 'type': 'float32', + 'fill': 0, +} +choices = { + 'model': ('joint', 'deform', 'affine', 'rigid'), + 'extent': (192, 256), + 'method': ('linear', 'nearest'), + 'type': ('uint8', 'uint16', 'int16', 'int32', 'float32'), +} +limits = { + 'steps': 5, +} +resolve = ('model', 'method') + + +# Documentation. +n = '\033[0m' if sys.stdout.isatty() else '' +b = '\033[1m' if sys.stdout.isatty() else '' +u = '\033[4m' if sys.stdout.isatty() else '' +prog = os.path.basename(sys.argv[0]) + + +# References. +ref = f''' +SynthMorph: learning contrast-invariant registration without acquired images\t +Hoffmann M, Billot B, Greve DN, Iglesias JE, Fischl B, Dalca AV\t +IEEE Transactions on Medical Imaging, 41 (3), 543-558, 2022\t +https://doi.org/10.1109/TMI.2021.3116879 + +Anatomy-specific acquisition-agnostic {u}affine{n} registration learned from fictitious images\t +Hoffmann M, Hoopes A, Fischl B*, Dalca AV* (*equal contribution)\t +SPIE Medical Imaging: Image Processing, 12464, 1246402, 2023\t +https://doi.org/10.1117/12.2653251\t +https://synthmorph.io/#papers (PDF) + +Anatomy-aware and acquisition-agnostic {u}joint{n} registration with SynthMorph\t +Hoffmann M, Hoopes A, Greve DN, Fischl B*, Dalca AV* (*equal contribution)\t +Imaging Neuroscience, 2, 1-33, 2024\t +https://doi.org/10.1162/imag_a_00197 + +Website: https://synthmorph.io +''' + +help_general = f'''{prog} + +{b}NAME{n} + {b}{prog}{n} - register 3D brain images without preprocessing + +{b}SYNOPSIS{n} + {b}{prog}{n} [-h] {u}command{n} [options] + +{b}DESCRIPTION{n} + SynthMorph is a deep-learning tool for symmetric, acquisition-agnostic + registration of single-frame brain MRI of any geometry. The + registration is anatomy-aware, removing the need for skull-stripping, + and you can control the warp smoothness. + + Pass an option or {u}command{n} from the following list. You can omit + trailing characters, as long as there is no ambiguity. + + {b}register{n} + Register 3D brain images without preprocessing. + + {b}apply{n} + Apply an existing transform to another 3D image or label map. + + {b}-h{n} + Print this help text and exit. + +{b}IMAGE FORMAT{n} + The registration supports single-frame image volumes of any size, + resolution, and orientation. The moving and the fixed image geometries + can differ. The accepted image file formats are: MGH (.mgz) and NIfTI + (.nii.gz, .nii). + + Internally, the registration converts image buffers to: isotropic 1-mm + voxels, intensities min-max normalized into the interval [0, 1], and + left-inferior-anterior (LIA) axes. This conversion requires intact + image-to-world matrices. That is, the head must have the correct + anatomical orientation in a viewer like {b}freeview{n}. + +{b}TRANSFORM FORMAT{n} + SynthMorph transformations operate in physical RAS space. We save + matrix transforms as text in LTA format (.lta) and displacement fields + as images with three frames indicating shifts in RAS direction. + +{b}ENVIRONMENT{n} + The following environment variables affect {b}{prog}{n}: + + SUBJECTS_DIR + Ignored unless {b}{prog}{n} runs inside a container. Mounts the + host directory SUBJECTS_DIR to {u}/mnt{n} inside the container. + Defaults to the current working directory. + +{b}SEE ALSO{n} + For converting, composing, and applying transforms, consider FreeSurfer + tools {b}lta_convert{n}, {b}mri_warp_convert{n}, + {b}mri_concatenate_lta{n}, {b}mri_concatenate_gcam{n}, and + {b}mri_convert{n}. + +{b}CONTACT{n} + Reach out to freesurfer@nmr.mgh.harvard.edu or at + https://voxelmorph.net. + +{b}REFERENCES{n} + If you use SynthMorph in a publication, please cite us! +''' + textwrap.indent(ref, prefix=' ' * 8) + +help_register = f'''{prog}-register + +{b}NAME{n} + {b}{prog}-register{n} - register 3D brain images without preprocessing + +{b}SYNOPSIS{n} + {b}{prog} register{n} [options] {u}moving{n} {u}fixed{n} + +{b}DESCRIPTION{n} + SynthMorph is a deep-learning tool for symmetric, acquisition-agnostic + registration of brain MRI with any volume size, resolution, and + orientation. The registration is anatomy-aware, removing the need for + skull-stripping, and you can control the warp smoothness. + + SynthMorph registers a {u}moving{n} (source) image to a {u}fixed{n} + (target) image. Their geometries can differ. The options are as + follows: + + {b}-m{n} {u}model{n} + Transformation model ({', '.join(choices['model'])}). Defaults + to {default['model']}. Joint includes affine and deformable but + differs from running both in sequence in that it applies the + deformable step in an affine mid-space to guarantee symmetric + joint transforms. Deformable assumes prior affine alignment or + initialization with {b}-i{n}. + + {b}-o{n} {u}image{n} + Save {u}moving{n} registered to {u}fixed{n}. + + {b}-O{n} {u}image{n} + Save {u}fixed{n} registered to {u}moving{n}. + + {b}-H{n} + Update the voxel-to-world matrix instead of resampling when + saving images with {b}-o{n} and {b}-O{n}. For matrix transforms + only. Not all software supports headers with shear from affine + registration. + + {b}-t{n} {u}trans{n} + Save the transform from {u}moving{n} to {u}fixed{n}, including + any initialization. + + {b}-T{n} {u}trans{n} + Save the transform from {u}fixed{n} to {u}moving{n}, including + any initialization. + + {b}-i{n} {u}trans{n} + Apply an initial matrix transform to {u}moving{n} before the + registration. + + {b}-M{n} + Apply half the initial matrix transform to {u}moving{n} and + (the inverse of) the other half to {u}fixed{n}, for symmetry. + This will make running the deformable after an affine step + equivalent to joint registration. Requires {b}-i{n}. + + {b}-j{n} {u}threads{n} + Number of TensorFlow threads. System default if unspecified. + + {b}-g{n} + Use the GPU in environment variable CUDA_VISIBLE_DEVICES or GPU + 0 if the variable is unset or empty. + + {b}-r{n} {u}lambda{n} + Regularization parameter in the open interval (0, 1) for + deformable registration. Higher values lead to smoother warps. + Defaults to {default['hyper']}. + + {b}-n{n} {u}steps{n} + Integration steps for deformable registration. Lower numbers + improve speed and memory use but can lead to inaccuracies and + folding voxels. Defaults to {default['steps']}. Should not be + less than {limits['steps']}. + + {b}-e{n} {u}extent{n} + Isotropic extent of the registration space in unit voxels + {choices['extent']}. Lower values improve speed and memory use + but may crop the anatomy of interest. Defaults to + {default['extent']}. + + {b}-w{n} {u}weights{n} + Use alternative model weights, exclusively. Repeat the flag + to set affine and deformable weights for joint registration, + or the result will disappoint. + + {b}-h{n} + Print this help text and exit. + +{b}ENVIRONMENT{n} + The following environment variables affect {b}{prog}-register{n}: + + CUDA_VISIBLE_DEVICES + Use a specific GPU. If unset or empty, passing {b}-g{n} will + select GPU 0. Ignored without {b}-g{n}. + + FREESURFER_HOME + Load model weights from directory {u}FREESURFER_HOME/models{n}. + Ignored when specifying weights with {b}-w{n}. + +{b}EXAMPLES{n} + Joint affine-deformable registration, saving the moved image: + # {prog} register -o out.nii mov.nii fix.nii + + Joint registration at 25% warp smoothness: + # {prog} register -r 0.25 -o out.nii mov.nii fix.nii + + Affine registration saving the transform: + # {prog} register -m affine -t aff.lta mov.nii.gz fix.nii.gz + + Deformable registration only, assuming prior affine alignment: + # {prog} register -m deform -t def.mgz mov.mgz fix.mgz + + Deformable step initialized with an affine transform: + # {prog} reg -m def -i aff.lta -o out.mgz mov.mgz fix.mgz + + Rigid registration, setting the output image header (no resampling): + # {prog} register -m rigid -Ho out.mgz mov.mgz fix.mgz +''' + +help_apply = f'''{prog}-apply + +{b}NAME{n} + {b}{prog}-apply{n} - apply an existing SynthMorph transform + +{b}SYNOPSIS{n} + {b}{prog} apply{n} [options] {u}trans{n} {u}image{n} {u}output{n} + [{u}image{n} {u}output{n} ...] + +{b}DESCRIPTION{n} + Apply a spatial transform {u}trans{n} estimated by SynthMorph to a 3D + {u}image{n} and write the result to {u}output{n}. You can pass any + number of image-output pairs to be processed in the same way. + + The following options identically affect all input-output pairs. + + {b}-H{n} + Update the voxel-to-world matrix of {u}output{n} instead of + resampling. For matrix transforms only. Not all software and + file formats support headers with shear from affine + registration. + + {b}-m{n} {u}method{n} + Interpolation method ({', '.join(choices['method'])}). Defaults + to {default['method']}. Choose linear for images and nearest + for label (segmentation) maps. + + {b}-t{n} {u}type{n} + Output data type ({', '.join(choices['type'])}). Defaults to + {default['type']}. Casting to a type other than + {default['type']} after linear interpolation may result in + information loss. + + {b}-f{n} {u}fill{n} + Extrapolation fill value for areas outside the field-of-view of + {u}image{n}. Defaults to {default['fill']}. + + {b}-h{n} + Print this help text and exit. + +{b}EXAMPLES{n} + Apply an affine transform to an image: + # {prog} apply affine.lta image.nii out.nii.gz + + Apply a warp to an image, saving the output in floating-point format: + # {prog} apply -t float32 warp.nii image.nii out.nii + + Apply the same transform to two images: + # {prog} app warp.mgz im_1.mgz out_1.mgz im_2.mgz out_2.mgz + + Transform a label map: + # {prog} apply -m nearest warp.nii labels.nii out.nii +''' + + +# Command-line parsing. +p = argparse.ArgumentParser() +p.format_help = lambda: utils.rewrap_text(help_general, end='\n\n') + +sub = p.add_subparsers(dest='command') +commands = {f: sub.add_parser(f) for f in ('register', 'apply')} + + +def add_flags(f): + out = {} + + if f in choices: + out.update(choices=choices[f]) + if f in resolve: + out.update(type=lambda x: utils.resolve_abbrev(x, strings=choices[f])) + + if f in default: + out.update(default=default[f]) + + return out + + +# Registration arguments. +r = commands['register'] +r.add_argument('moving') +r.add_argument('fixed') +r.add_argument('-m', dest='model', **add_flags('model')) +r.add_argument('-o', dest='out_moving', metavar='image') +r.add_argument('-O', dest='out_fixed', metavar='image') +r.add_argument('-H', dest='header_only', action='store_true') +r.add_argument('-t', dest='trans', metavar='trans') +r.add_argument('-T', dest='inverse', metavar='trans') +r.add_argument('-i', dest='init', metavar='trans') +r.add_argument('-M', dest='mid_space', action='store_true') +r.add_argument('-j', dest='threads', metavar='threads', type=int) +r.add_argument('-g', dest='gpu', action='store_true') +r.add_argument('-r', dest='hyper', metavar='lambda', type=float, **add_flags('hyper')) +r.add_argument('-n', dest='steps', metavar='steps', type=int, **add_flags('steps')) +r.add_argument('-e', dest='extent', type=int, **add_flags('extent')) +r.add_argument('-w', dest='weights', metavar='weights', action='append') +r.add_argument('-v', dest='verbose', action='store_true') +r.add_argument('-d', dest='out_dir', metavar='dir', type=pathlib.Path) +r.format_help = lambda: utils.rewrap_text(help_register, end='\n\n') + +# Transformation arguments. +a = commands['apply'] +a.add_argument('trans') +a.add_argument('pairs', metavar='image output', nargs='+') +a.add_argument('-H', dest='header_only', action='store_true') +a.add_argument('-m', dest='method', **add_flags('method')) +a.add_argument('-t', dest='type', **add_flags('type')) +a.add_argument('-f', dest='fill', metavar='fill', type=float, **add_flags('fill')) +a.format_help = lambda: utils.rewrap_text(help_apply, end='\n\n') + + +# Parse arguments. +if len(sys.argv) == 1: + p.print_usage() + exit(0) + +# Command resolution. +c = sys.argv[1] = utils.resolve_abbrev(sys.argv[1], commands) +if len(sys.argv) == 2 and c in commands: + commands[c].print_usage() + exit(0) + +# Default command. +if c not in (*commands, '-h'): + sys.argv.insert(1, 'register') + +arg = p.parse_args() + + +# Command. +if arg.command == 'register': + + # Argument checking. + if arg.header_only and not arg.model in ('affine', 'rigid'): + sf.system.fatal('-H is not compatible with deformable registration') + + if arg.mid_space and not arg.init: + sf.system.fatal('-M requires matrix initialization') + + if not 0 < arg.hyper < 1: + sf.system.fatal('regularization strength not in open interval (0, 1)') + + if arg.steps < limits['steps']: + sf.system.fatal('too few integration steps') + + # TensorFlow setup. + gpu = os.environ.get('CUDA_VISIBLE_DEVICES', '0') + os.environ['CUDA_VISIBLE_DEVICES'] = gpu if arg.gpu else '' + os.environ['TF_CPP_MIN_LOG_LEVEL'] = '0' if arg.verbose else '3' + os.environ['NEURITE_BACKEND'] = 'tensorflow' + os.environ['VXM_BACKEND'] = 'tensorflow' + print(arg) + from synthmorph import registration + + registration.register(arg) + + +if arg.command == 'apply': + + # Argument checking. + which = 'affine' if arg.trans.endswith('.lta') else 'warp' + if arg.header_only and which == 'warp': + sf.system.fatal('-H is not compatible with deformable transforms') + + if len(arg.pairs) % 2: + sf.system.fatal('list of input-output pairs not of even length') + + # Transform. + trans = getattr(sf, f'load_{which}')(arg.trans) + prop = dict(method=arg.method, resample=not arg.header_only, fill=arg.fill) + for inp, out in zip(arg.pairs[::2], arg.pairs[1::2]): + sf.load_volume(inp).transform(trans, **prop).astype(arg.type).save(out) + + +print('Thank you for choosing SynthMorph. Please cite us!') +print(utils.rewrap_text(ref)) diff --git a/onlylian/mri_synthmorph/mri_synthmorph_apply b/onlylian/mri_synthmorph/mri_synthmorph_apply new file mode 100755 index 0000000..e6b1d09 --- /dev/null +++ b/onlylian/mri_synthmorph/mri_synthmorph_apply @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 + +import os +import sys +import shutil +import textwrap +import argparse +import surfa as sf + + +# Settings. +default = { + 'method': 'linear', + 'fill': 0, + 'type': 'float32', +} +choices = { + 'method': ('linear', 'nearest'), + 'type': ('uint8', 'uint16', 'int16', 'int32', 'float32'), +} + + +def rewrap(text, width=None, hard='\t\n', hard_indent=0): + """Rewrap text such that lines fill the available horizontal space. + + Reformats individual paragraphs of a text body, considering subsequent + lines with identical indentation as paragraphs. For unspecified width, the + function will attempt to determine the extent of the terminal. + + Parameters + ---------- + text : str + Text to rewrap. + width : int, optional + Maximum line width. None means the width of the terminal as determined + by `textwrap`, defaulting to 80 characters for background processes. + hard : str, optional + String interpreted as a hard break when terminating a line. Useful for + inserting a line break without changing the indentation level. Must end + with a line break and will be removed from the output text. + hard_indent : int, optional + Number of additional whitespace characters by which to indent the lines + following a hard break. See `hard`. + + Returns + ------- + out : str + Reformatted text. + + """ + # Inputs. + if width is None: + width = shutil.get_terminal_size().columns + lines = text.splitlines(keepends=True) + + # Merge lines to paragraphs. + pad = [] + pad_hard = [] + par = [] + for i, line in enumerate(lines): + ind = len(line) - len(line.lstrip()) + if i == 0 or ind != pad[-1] or lines[i - 1].endswith(hard): + par.append('') + pad.append(ind) + pad_hard.append(ind) + + if line.endswith(hard): + line = line.replace(hard, '\n') + pad_hard[-1] += hard_indent + par[-1] += line[ind:] + + # Reformat paragraphs. + for i, _ in enumerate(par): + par[i] = textwrap.fill( + par[i], width, + initial_indent=' ' * pad[i], subsequent_indent=' ' * pad_hard[i], + ) + + return '\n'.join(par) + + +# Documentation. +n = '\033[0m' if sys.stdout.isatty() else '' +b = '\033[1m' if sys.stdout.isatty() else '' +u = '\033[4m' if sys.stdout.isatty() else '' +prog = os.path.basename(sys.argv[0]) +doc = f'''{prog} + +{b}NAME{n} + {b}{prog}{n} - apply a SynthMorph transform to 3D images + +{b}SYNOPSIS{n} + {b}{prog}{n} [options] {u}trans{n} {u}image{n} {u}output{n} + [{u}image{n} {u}output{n} ...] + +{b}DESCRIPTION{n} + Apply a spatial transform {u}trans{n} estimated by SynthMorph to a 3D + {u}image{n} and write the result to {u}output{n}. You can pass any + number of image-output pairs to be processed in the same way. + + The following options identically affect all image-output pairs. + + {b}-H{n} + Update the voxel-to-world matrix of the output image instead of + resampling. For matrix transforms only. Not all software and + file formats support headers with shear from affine + registration. + + {b}-i{n} {u}method{n} + Interpolation method ({', '.join(choices['method'])}). Defaults + to {default['method']}. Choose linear for images and nearest + for label (segmentation) maps. + + {b}-t{n} {u}type{n} + Output data type ({', '.join(choices['type'])}). Defaults to + {default['type']}. Casting to a narrower type can result in + information loss. + + {b}-f{n} {u}fill{n} + Extrapolation fill value for areas outside the field-of-view of + the input image. Defaults to {default['fill']}. + + {b}-h{n} + Print this help text and exit. + +{b}IMAGE FORMAT{n} + Accepted file formats include: MGH (.mgz) and NIfTI (.nii.gz, .nii). + +{b}TRANSFORMS{n} + Refer to the help text of the registration utility for information on + transform file formats. + + For converting, composing, and applying transforms, consider the + FreeSurfer tools lta_convert, mri_warp_convert, mri_concatenate_lta, + mri_concatenate_gcam, mri_convert, mri_info. + +{b}ENVIRONMENT{n} + The following environment variables affect {b}{prog}{n}: + + SUBJECTS_DIR + Ignored unless {b}{prog}{n} runs inside a container. Mount the + host directory SUBJECTS_DIR to {u}/mnt{n} inside the container. + Defaults to the current working directory. + +{b}EXAMPLES{n} + Apply an affine transform to an image: + # {prog} affine.lta image.nii out.nii.gz + + Apply a warp to an image, saving the output in floating-point format: + # {prog} -t float32 warp.mgz image.mgz out.mgz + + Apply a transform to each of two images: + # {prog} warp.mgz image_1.mgz out_1.mgz image_2.mgz out_2.mgz + + Transform a label map: + # {prog} -i nearest warp.mgz labels.mgz out.mgz + +{b}CONTACT{n} + Reach out to freesurfer@nmr.mgh.harvard.edu or at + https://github.com/voxelmorph/voxelmorph. + +{b}REFERENCES{n} + If you use SynthMorph in a publication, please cite us! +''' + + +# References. +ref = ''' +SynthMorph: learning contrast-invariant registration without acquired images\t +Hoffmann M, Billot B, Greve DN, Iglesias JE, Fischl B, Dalca AV\t +IEEE Transactions on Medical Imaging, 41 (3), 543-558, 2022\t +https://doi.org/10.1109/TMI.2021.3116879 + +Anatomy-specific acquisition-agnostic affine registration learned from fictitious images\t +Hoffmann M, Hoopes A, Fischl B*, Dalca AV* (*equal contribution)\t +SPIE Medical Imaging: Image Processing, 12464, 1246402, 2023\t +https://doi.org/10.1117/12.2653251\t +https://synthmorph.io/#papers (PDF) + +Anatomy-aware and acquisition-agnostic joint registration with SynthMorph\t +Hoffmann M, Hoopes A, Greve DN, Fischl B*, Dalca AV* (*equal contribution)\t +Imaging Neuroscience, 2, 1-33, 2024\t +https://doi.org/10.1162/imag_a_00197 + +Website: https://synthmorph.io +''' +doc += textwrap.indent(ref, prefix=' ' * 8) + + +print(rewrap(( + f'Warning: {prog} is deprecated in favor of `mri_synthmorph apply` and ' + 'will be removed in the future.' +))) + + +# Arguments. +p = argparse.ArgumentParser(add_help=False) +p.add_argument('trans') +p.add_argument('pairs', metavar='image output', nargs='+') +p.add_argument('-H', dest='header_only', action='store_true') +p.add_argument('-i', dest='method', choices=choices['method'], default=default['method']) +p.add_argument('-t', dest='type', choices=choices['type'], default=default['type']) +p.add_argument('-f', dest='fill', metavar='fill', type=float, default=default['fill']) +p.add_argument('-h', action='store_true') + + +# Help. +if len(sys.argv) == 1: + p.print_usage() + exit(0) + +if any(f[0] == '-' and 'h' in f for f in sys.argv): + print(rewrap(doc), end='\n\n') + exit(0) + + +# Parsing. +arg = p.parse_args() + +if len(arg.pairs) % 2: + sf.system.fatal('did not receive even-length list of input-output pairs') + + +# Transform. +f = 'affine' if arg.trans.endswith('.lta') else 'warp' +trans = getattr(sf, f'load_{f}')(arg.trans) + + +# Application. +pairs = zip(arg.pairs[::2], arg.pairs[1::2]) +prop = dict(method=arg.method, resample=not arg.header_only, fill=arg.fill) +for inp, out in pairs: + sf.load_volume(inp).transform(trans, **prop).astype(arg.type).save(out) + + +print('Thank you for choosing SynthMorph. Please cite us!') +print(rewrap(ref)) diff --git a/onlylian/mri_synthmorph/synthmorph/registration.py b/onlylian/mri_synthmorph/synthmorph/registration.py new file mode 100755 index 0000000..7351fa1 --- /dev/null +++ b/onlylian/mri_synthmorph/synthmorph/registration.py @@ -0,0 +1,313 @@ +import os +import h5py +import numpy as np +import surfa as sf +import tensorflow as tf +import voxelmorph as vxm + + +# Settings. +weights = { + 'joint': ('synthmorph.affine.2.h5', 'synthmorph.deform.3.h5',), + 'deform': ('synthmorph.deform.3.h5',), + 'affine': ('synthmorph.affine.2.h5',), + 'rigid': ('synthmorph.rigid.1.h5',), +} + + +def network_space(im, shape, center=None): + """Construct transform from network space to the voxel space of an image. + + Constructs a coordinate transform from the space the network will operate + in to the zero-based image index space. The network space has isotropic + 1-mm voxels, left-inferior-anterior (LIA) orientation, and no shear. It is + centered on the field of view, or that of a reference image. This space is + an indexed voxel space, not world space. + + Parameters + ---------- + im : surfa.Volume + Input image to construct the transform for. + shape : (3,) array-like + Spatial shape of the network space. + center : surfa.Volume, optional + Center the network space on the center of a reference image. + + Returns + ------- + out : tuple of (3, 4) NumPy arrays + Transform from network to input-image space and its inverse, thinking + coordinates. + + """ + old = im.geom + new = sf.ImageGeometry( + shape=shape, + voxsize=1, + rotation='LIA', + center=old.center if center is None else center.geom.center, + shear=None, + ) + + net_to_vox = old.world2vox @ new.vox2world + vox_to_net = new.world2vox @ old.vox2world + return net_to_vox.matrix, vox_to_net.matrix + + +def transform(im, trans, shape=None, normalize=False, batch=False): + """Apply a spatial transform to 3D image voxel data in dimensions. + + Applies a transformation matrix operating in zero-based index space or a + displacement field to an image buffer. + + Parameters + ---------- + im : surfa.Volume or NumPy array or TensorFlow tensor + Input image to transform, without batch dimension. + trans : array-like + Transform to apply to the image. A matrix of shape (3, 4), a matrix + of shape (4, 4), or a displacement field of shape (*space, 3), + without batch dimension. + shape : (3,) array-like, optional + Output shape used for converting matrices to dense transforms. None + means the shape of the input image will be used. + normalize : bool, optional + Min-max normalize the image intensities into the interval [0, 1]. + batch : bool, optional + Prepend a singleton batch dimension to the output tensor. + + Returns + ------- + out : float TensorFlow tensor + Transformed image with a trailing feature dimension. + + """ + # Add singleton feature dimension if needed. + if tf.rank(im) == 3: + im = im[..., tf.newaxis] + + out = vxm.utils.transform( + im, trans, fill_value=0, shift_center=False, shape=shape, + ) + + if normalize: + out -= tf.reduce_min(out) + out /= tf.reduce_max(out) + + if batch: + out = out[tf.newaxis, ...] + + return out + + +def load_weights(model, weights): + """Load weights into model or submodel. + + Attempts to load (all) weights into a model or one of its submodels. If + that fails, `model` may be a submodel of what we got weights for, and we + attempt to load the weights of a submodel (layer) into `model`. + + Parameters + ---------- + model : TensorFlow model + Model to initialize. + weights : str or pathlib.Path + Path to weights file. + + Raises + ------ + ValueError + If unsuccessful at loading any weights. + + """ + # Extract submodels. + models = [model] + i = 0 + while i < len(models): + layers = [f for f in models[i].layers if isinstance(f, tf.keras.Model)] + models.extend(layers) + i += 1 + + # Add models wrapping a single model in case this was done in training. + # Requires list expansion or Python will get stuck. + models.extend([tf.keras.Model(m.inputs, m(m.inputs)) for m in models]) + + # Attempt to load all weights into one of the models. + for mod in models: + try: + mod.load_weights(weights) + return + except ValueError as e: + pass + + # Assume `model` is a submodel of what we got weights for. + with h5py.File(weights, mode='r') as h5: + layers = h5.attrs['layer_names'] + weights = [list(h5[lay].attrs['weight_names']) for lay in layers] + + # Layers with weights. Attempt loading. + layers, weights = zip(*filter(lambda f: f[1], zip(layers, weights))) + for lay, wei in zip(layers, weights): + try: + model.set_weights([h5[lay][w] for w in wei]) + return + except ValueError as e: + if lay is layers[-1]: + raise e + + +def register(arg): + + # Parse arguments. + in_shape = (arg.extent,) * 3 + is_mat = arg.model in ('affine', 'rigid') + + # Threading. + if arg.threads: + tf.config.threading.set_inter_op_parallelism_threads(arg.threads) + tf.config.threading.set_intra_op_parallelism_threads(arg.threads) + + # Input data. + mov = sf.load_volume(arg.moving) + fix = sf.load_volume(arg.fixed) + if not len(mov.shape) == len(fix.shape) == 3: + sf.system.fatal('input images are not single-frame volumes') + + # Transforms between native voxel and network coordinates. Voxel and + # network spaces differ for each image. The networks expect isotropic 1-mm + # LIA spaces. Center these on the original images, except in the deformable + # case: it assumes prior affine registration, so we center the moving + # network space on the fixed image, to take into account affine transforms + # via resampling, updating the header, or passed on the command line alike. + center = fix if arg.model == 'deform' else None + net_to_mov, mov_to_net = network_space(mov, shape=in_shape, center=center) + net_to_fix, fix_to_net = network_space(fix, shape=in_shape) + + # Coordinate transforms from and to world space. There is only one world. + mov_to_ras = mov.geom.vox2world.matrix + fix_to_ras = fix.geom.vox2world.matrix + ras_to_mov = mov.geom.world2vox.matrix + ras_to_fix = fix.geom.world2vox.matrix + + # Incorporate an initial matrix transform from moving to fixed coordinates, + # as LTAs store the inverse. For mid-space initialization, compute the + # square root of the transform between fixed and moving network space. + if arg.init: + init = sf.load_affine(arg.init).convert(space='voxel') + if init.ndim != 3 \ + or not sf.transform.image_geometry_equal(mov.geom, init.source, tol=1e-3) \ + or not sf.transform.image_geometry_equal(fix.geom, init.target, tol=1e-3): + sf.system.fatal('initial transform geometry does not match images') + + init = fix_to_net @ init @ net_to_mov + if arg.mid_space: + init = tf.linalg.sqrtm(init) + if np.any(np.isnan(init)): + sf.system.fatal(f'cannot compute matrix square root of {arg.init}') + net_to_fix = net_to_fix @ init + fix_to_net = np.linalg.inv(net_to_fix) + + net_to_mov = net_to_mov @ tf.linalg.inv(init) + mov_to_net = np.linalg.inv(net_to_mov) + + # Take the input images to network space. When saving the moving image with + # the correct voxel-to-RAS matrix after incorporating an initial transform, + # an image viewer taking this matrix into account will show an unchanged + # image. The networks only see the voxel data, which have been moved. + inputs = ( + transform(mov, net_to_mov, shape=in_shape, normalize=True, batch=True), + transform(fix, net_to_fix, shape=in_shape, normalize=True, batch=True), + ) + + # Network. For deformable-only registration, `HyperVxmJoint` ignores the + # `mid_space` argument, and the initialization will determine the space. + prop = dict(in_shape=in_shape, bidir=True) + if is_mat: + prop.update(make_dense=False, rigid=arg.model == 'rigid') + model = vxm.networks.VxmAffineFeatureDetector(**prop) + + else: + prop.update(mid_space=True, int_steps=arg.steps, skip_affine=arg.model == 'deform') + model = vxm.networks.HyperVxmJoint(**prop) + inputs = (tf.constant([arg.hyper]), *inputs) + + # Weights. + if not arg.weights: + fs = os.environ.get('FREESURFER_HOME') + if not fs: + sf.system.fatal('set environment variable FREESURFER_HOME or weights') + arg.weights = [os.path.join(fs, 'models', f) for f in weights[arg.model]] + + for f in arg.weights: + load_weights(model, weights=f) + + # Inference. The first transform maps from the moving to the fixed image, + # or equivalently, from fixed to moving coordinates. The second is the + # inverse. Convert transforms between moving and fixed network spaces to + # transforms between the original voxel spaces. + pred = tuple(map(tf.squeeze, model(inputs))) + fw, bw = pred + fw = vxm.utils.compose((net_to_mov, fw, fix_to_net), shift_center=False, shape=fix.shape) + bw = vxm.utils.compose((net_to_fix, bw, mov_to_net), shift_center=False, shape=mov.shape) + + # Associate image geometries with the transforms. LTAs store the inverse. + if is_mat: + fw, bw = bw, fw + fw = sf.Affine(fw, source=mov, target=fix, space='voxel') + bw = sf.Affine(bw, source=fix, target=mov, space='voxel') + format = dict(space='world') + + else: + fw = sf.Warp(fw, source=mov, target=fix, format=sf.Warp.Format.disp_crs) + bw = sf.Warp(bw, source=fix, target=mov, format=sf.Warp.Format.disp_crs) + format = dict(format=sf.Warp.Format.disp_ras) + + # Output transforms. + if arg.trans: + fw.convert(**format).save(arg.trans) + + if arg.inverse: + bw.convert(**format).save(arg.inverse) + + # Moved images. + if arg.out_moving: + mov.transform(fw, resample=not arg.header_only).save(arg.out_moving) + + if arg.out_fixed: + fix.transform(bw, resample=not arg.header_only).save(arg.out_fixed) + + # Outputs in network space. + if arg.out_dir: + arg.out_dir.mkdir(exist_ok=True) + + # Input images. + mov = sf.ImageGeometry(in_shape, vox2world=mov_to_ras @ net_to_mov) + fix = sf.ImageGeometry(in_shape, vox2world=fix_to_ras @ net_to_fix) + mov = sf.Volume(inputs[-2][0], geometry=fix if arg.init else mov) + fix = sf.Volume(inputs[-1][0], geometry=fix) + mov.save(filename=arg.out_dir / 'inp_1.nii.gz') + fix.save(filename=arg.out_dir / 'inp_2.nii.gz') + + fw, bw = pred + if is_mat: + fw, bw = bw, fw + fw = sf.Affine(fw, source=mov, target=fix, space='voxel') + bw = sf.Affine(bw, source=fix, target=mov, space='voxel') + ext = 'lta' + + else: + fw = sf.Warp(fw, source=mov, target=fix, format=sf.Warp.Format.disp_crs) + bw = sf.Warp(bw, source=fix, target=mov, format=sf.Warp.Format.disp_crs) + ext = 'nii.gz' + + # Transforms. + fw.convert(**format).save(filename=arg.out_dir / f'tra_1.{ext}') + bw.convert(**format).save(filename=arg.out_dir / f'tra_2.{ext}') + + # Moved images. + mov.transform(fw).save(filename=arg.out_dir / 'out_1.nii.gz') + fix.transform(bw).save(filename=arg.out_dir / 'out_2.nii.gz') + + vmpeak = sf.system.vmpeak() + if vmpeak is not None: + print(f'#@# mri_synthmorph: {arg.model}, threads: {arg.threads}, VmPeak: {vmpeak}') diff --git a/onlylian/mri_synthmorph/synthmorph/utils.py b/onlylian/mri_synthmorph/synthmorph/utils.py new file mode 100755 index 0000000..6ca415b --- /dev/null +++ b/onlylian/mri_synthmorph/synthmorph/utils.py @@ -0,0 +1,93 @@ +import shutil +import textwrap + + +def resolve_abbrev(needle, strings, lower=False): + """Return a full-length string matching a substring from the beginning. + + Parameters + ---------- + needle : str + Substring of one of several `strings`. + strings : str or iterable of str + Full-length strings, one of which should begin with `needle`. + lower : bool, optional + Convert needle to lowercase before matching. + + Returns + ------- + str + String in `strings` that begins with `needle` if there is no ambiguity. + If there is not exactly one match, the function will return `needle`. + + """ + if isinstance(strings, str): + strings = [strings] + strings = tuple(strings) + + if lower: + needle = needle.lower() + + matches = [f for f in strings if f.startswith(needle)] + return matches[0] if len(matches) == 1 else needle + + +def rewrap_text(text, width=None, hard='\t\n', hard_indent=0, end=''): + """Rewrap text such that lines fill the available horizontal space. + + Reformats individual paragraphs of a text body, considering subsequent + lines with identical indentation as paragraphs. For unspecified width, the + function will attempt to determine the extent of the terminal. + + Parameters + ---------- + text : str + Text to rewrap. + width : int, optional + Maximum line width. None means the width of the terminal as determined + by `textwrap`, defaulting to 80 characters for background processes. + hard : str, optional + String interpreted as a hard break when terminating a line. Useful for + inserting a line break without changing the indentation level. Must end + with a line break and will be removed from the output text. + hard_indent : int, optional + Number of additional whitespace characters by which to indent the lines + following a hard break. See `hard`. + end : str, optional + Append to the reformatted text. + + Returns + ------- + out : str + Reformatted text. + + """ + # Inputs. + if width is None: + width = shutil.get_terminal_size().columns + lines = text.splitlines(keepends=True) + + # Merge lines to paragraphs. + pad = [] + pad_hard = [] + par = [] + for i, line in enumerate(lines): + ind = len(line) - len(line.lstrip()) + if i == 0 or ind != pad[-1] or lines[i - 1].endswith(hard): + par.append('') + pad.append(ind) + pad_hard.append(ind) + + if line.endswith(hard): + line = line.replace(hard, '\n') + pad_hard[-1] += hard_indent + par[-1] += line[ind:] + + # Reformat paragraphs. + for i, _ in enumerate(par): + par[i] = textwrap.fill( + par[i], width, + initial_indent=' ' * pad[i], subsequent_indent=' ' * pad_hard[i], + ) + + return '\n'.join(par) + end diff --git a/onlylian/mri_synthmorph/test_apply.sh b/onlylian/mri_synthmorph/test_apply.sh new file mode 100755 index 0000000..f77e6de --- /dev/null +++ b/onlylian/mri_synthmorph/test_apply.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +. "$(dirname "$0")/../test.sh" + +t() { test_command mri_synthmorph "$@" ; } + +# image interpolation +t apply affine.lta moving.mgz out.mgz +compare_vol out.mgz affine.mgz --thresh 0.02 --res-thresh 1e-3 --geo-thresh 1e-3 + +# NIfTI format +t apply affine.lta moving.mgz out.nii.gz +compare_vol out.nii.gz affine.mgz --thresh 0.02 --res-thresh 1e-3 --geo-thresh 1e-3 + +# dense transform +t apply identity.mgz moving.mgz out.mgz -t uint8 +compare_vol out.mgz moving.mgz --res-thresh 1e-3 --geo-thresh 1e-3 + +# matrix update +t apply -H rigid.lta -t uint8 moving.mgz out.mgz +compare_vol out.mgz header.mgz --res-thresh 1e-3 --geo-thresh 1e-3 + +# label interpolation +t apply -m nearest -t uint8 affine.lta labels.moving.mgz out.mgz +compare_vol out.mgz labels.affine.mgz --res-thresh 1e-3 --geo-thresh 1e-3 + +# multiple input pairs +t apply rigid.lta moving.mgz out_1.mgz moving.mgz out_2.mgz +compare_vol out_1.mgz rigid.mgz --thresh 0.02 --res-thresh 1e-3 --geo-thresh 1e-3 +compare_vol out_2.mgz rigid.mgz --thresh 0.02 --res-thresh 1e-3 --geo-thresh 1e-3 + +# data types, command abbreviation +t a -t uint8 rigid.lta moving.mgz out.mgz +run_comparison mri_info --type out.mgz | grep -Fx uchar + +t ap -t uint16 rigid.lta moving.mgz out.mgz +run_comparison mri_info --type out.mgz | grep -Fx ushrt + +t app -t int16 rigid.lta moving.mgz out.mgz +run_comparison mri_info --type out.mgz | grep -Fx short + +t appl -t int32 rigid.lta moving.mgz out.mgz +run_comparison mri_info --type out.mgz | grep -Fx int + +t apply -t float32 rigid.lta moving.mgz out.mgz +run_comparison mri_info --type out.mgz | grep -Fx float + +# method abbreviation +FSTEST_NO_DATA_RESET=1 +for method in l li lin line linea linear n ne nea near neare neares nearest; do + t apply -m "$method" rigid.lta moving.mgz out.mgz +done + +# usage, help +t +t -h +t apply +t apply -h + +# NIfTI warp +FSTEST_NO_DATA_RESET=1 +mri_convert identity.mgz identity.nii.gz +t apply identity.nii.gz moving.mgz out.mgz -t uint8 +compare_vol out.mgz moving.mgz --res-thresh 1e-3 --geo-thresh 1e-3 + +# illegal arguments +EXPECT_FAILURE=1 +t slice +t apply -H identity.mgz moving.mgz fixed.mgz +t apply affine.lta moving.mgz out.mgz odd-number-of-io-pairs.mgz diff --git a/onlylian/mri_synthmorph/test_register.sh b/onlylian/mri_synthmorph/test_register.sh new file mode 100755 index 0000000..fb0a4e7 --- /dev/null +++ b/onlylian/mri_synthmorph/test_register.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +. "$(dirname "$0")/../test.sh" + +t() { test_command mri_synthmorph "$@" ; } + +# affine registration +t -m affine -o out.mgz moving.mgz fixed.mgz +compare_vol out.mgz affine.mgz --thresh 0.02 + +# affine symmetry +t -m aff -O out.mgz fixed.mgz moving.mgz +compare_vol out.mgz affine.mgz --thresh 0.02 + +# affine inverse consistency +t -m a -t out.lta -T inv.lta moving.mgz fixed.mgz +lta_diff out.lta inv.lta --invert2 | awk 'END {print $0; exit !($0<1e-3)}' + +# output directory creation +t -ma -d outputs fixed.mgz moving.mgz +[ -d outputs ] + +# rigid inverse consistency +t -m rigid -t out.lta -T inv.lta moving.mgz fixed.mgz +lta_diff out.lta inv.lta --invert2 | awk 'END {print $0; exit !($0<1e-3)}' + +# rigid registration +t -m rig -o out.mgz moving.mgz fixed.mgz +compare_vol out.mgz rigid.mgz --thresh 0.02 + +# geometry update +t -m r -Ho out.mgz moving.mgz fixed.mgz +compare_vol out.mgz header.mgz --thresh 0.02 --res-thresh 1e-3 --geo-thresh 1e-3 + +# deformable registration with initialization +t -m deform -i affine.lta -o out_1.mgz -O out_2.mgz moving.mgz fixed.mgz +compare_vol out_1.mgz deform_1.mgz --thresh 0.02 +compare_vol out_2.mgz deform_2.mgz --thresh 0.02 + +# deformable registration with mid-space initialization +t -m def -Mi affine.lta -o out.nii.gz moving.mgz fixed.mgz +compare_vol out.nii.gz deform_mid.nii.gz --thresh 0.02 --geo-thresh 1e-4 + +# joint registration +t -m joint -o out.mgz moving.mgz fixed.mgz +compare_vol out.mgz joint.mgz --thresh 0.02 + +# default model +t -o out.mgz moving.mgz fixed.mgz +compare_vol out.mgz joint.mgz --thresh 0.02 + +# help, usage, command abbreviation +FSTEST_NO_DATA_RESET=1 +t register -h +for cmd in r re reg regi regis registe register; do + t "$cmd" +done + +# deformable flags, explicit command +t register moving.mgz fixed.mgz -md -j16 -e256 -n7 -r0.5 +t register moving.mgz fixed.mgz -m j -j 16 -e 192 -n 5 -r 0.7 + +# NIfTI warps +FSTEST_NO_DATA_RESET=1 +mri_convert=$(find_path $FSTEST_CWD mri_convert/mri_convert) +t -t out.nii.gz moving.mgz fixed.mgz +test_command $mri_convert -odt float -at out.nii.gz moving.mgz out.mgz +compare_vol out.mgz joint.mgz --thresh 1 + +# displacements in RAS space +mri_warp_convert=$(find_path $FSTEST_CWD mri_warp_convert/mri_warp_convert) +test_command $mri_warp_convert -g moving.mgz --inras out.nii.gz --outmgzwarp out.mgz +test_command $mri_convert -odt float -at out.mgz moving.mgz moved.mgz +compare_vol moved.mgz joint.mgz --thresh 1 --geo-thresh 1e-4 + +# illegal arguments +EXPECT_FAILURE=1 +t moving.mgz fixed.mgz -m banana +t moving.mgz fixed.mgz -e 1 +t moving.mgz fixed.mgz -n 4 +t moving.mgz fixed.mgz -r 0 +t moving.mgz fixed.mgz -r 1 +t moving.mgz fixed.mgz -Hm deform +t moving.mgz fixed.mgz -Hm joint diff --git a/onlylian/mri_synthmorph/testdata.tar.gz b/onlylian/mri_synthmorph/testdata.tar.gz new file mode 100755 index 0000000..4aea9ee --- /dev/null +++ b/onlylian/mri_synthmorph/testdata.tar.gz @@ -0,0 +1 @@ +../.git/annex/objects/wx/GP/SHA256E-s4349681--04215d8b1685ab94d942b932b07b22da1b46ad8b03fda20f38f01c1740cf5c61.tar.gz/SHA256E-s4349681--04215d8b1685ab94d942b932b07b22da1b46ad8b03fda20f38f01c1740cf5c61.tar.gz \ No newline at end of file diff --git a/onlylian/nii_to_dicom.py b/onlylian/nii_to_dicom.py new file mode 100755 index 0000000..46a1024 --- /dev/null +++ b/onlylian/nii_to_dicom.py @@ -0,0 +1,291 @@ +import os +from typing import Optional + +import pyradise.data as ps_data +import pyradise.fileio as ps_io + +########## + +class ExampleModalityExtractor(ps_io.ModalityExtractor): + + def extract_from_dicom(self, + path: str + ) -> Optional[ps_data.Modality]: + # Extract the necessary attributes from the DICOM file + tags = (ps_io.Tag((0x0008, 0x0060)), # Modality + ps_io.Tag((0x0008, 0x103e))) # Series Description + dataset_dict = self._load_dicom_attributes(tags, path) + + # Identify the modality rule-based + modality = dataset_dict.get('Modality', {}).get('value', None) + series_desc = dataset_dict.get('Series Description', {}).get('value', '') + if modality == 'MR': + if 't1' in series_desc.lower(): + return ps_data.Modality('T1') + elif 't2' in series_desc.lower(): + return ps_data.Modality('T2') + else: + return None + else: + return None + + def extract_from_path(self, + path: str + ) -> Optional[ps_data.Modality]: + # Identify the discrete image file's modality rule-based + filename = os.path.basename(path) + + # Check if the image contains an img prefix + # (i.e., it is a intensity image) + if not filename.startswith('img'): + return None + + # Check if the image contains a modality search string + if 'T1' in filename: + return ps_data.Modality('T1') + elif 'T2' in filename: + return ps_data.Modality('T2') + else: + return None + + +class ExampleOrganExtractor(ps_io.OrganExtractor): + + def extract(self, + path: str + ) -> Optional[ps_data.Organ]: + # Identify the discrete image file's organ rule-based + filename = os.path.basename(path) + + # Check if the image contains a seg prefix + # (i.e., it is a segmentation) + if not filename.startswith('seg'): + return None + + # Split the filename for extracting the organ name + organ_name = filename.split('_')[-1].split('.')[0] + return ps_data.Organ(organ_name) + + +class ExampleAnnotatorExtractor(ps_io.AnnotatorExtractor): + + def extract(self, + path: str + ) -> Optional[ps_data.Annotator]: + # Identify the discrete image file's annotator rule-based + filename = os.path.basename(path) + + # Check if the image contains a seg prefix + # (i.e., it is a segmentation) + if not filename.startswith('seg'): + return None + + # Split the filename for extracting the annotator name + annotator_name = filename.split('_')[2] + return ps_data.Annotator(annotator_name) + +########## + +def convert_subject_to_dicom_rtss(input_dir_path: str, + output_dir_path: str, + dicom_image_dir_path: str, + use_3d_conversion: bool = True + ) -> None: + # Specify a reference modalities + # This is the modality of the DICOM image series that will be + # referenced in the DICOM-RTSS. + reference_modality = 'T1' + + # Create the loader + loader = ps_io.SubjectLoader() + + # Create the writer and specify the output file name of the + # DICOM-RTSS files + writer = ps_io.DicomSeriesSubjectWriter() + rtss_filename = 'rtss.dcm' + + # (optional) + # Instantiate a new selection to exclude the original DICOM-RTSS SeriesInfo + # Note: If this is omitted the original DICOM-RTSS will be copied to the + # corresponding output directory. + selection = ps_io.NoRTSSInfoSelector() + + # Create the file crawler for the discrete image files and + # loop through the subjects + crawler = ps_io.DatasetFileCrawler(input_dir_path, + extension='.nii.gz', + modality_extractor=ExampleModalityExtractor(), + organ_extractor=ExampleOrganExtractor(), + annotator_extractor=ExampleAnnotatorExtractor()) + for series_info in crawler: + # Load the subject + subject = loader.load(series_info) + + # Print the progress + print(f'Converting subject {subject.get_name()}...') + + # Construct the path to the subject's DICOM images + dicom_subject_path = os.path.join(dicom_image_dir_path, subject.get_name()) + + # Construct a DICOM crawler to retrieve the reference + # DICOM image series info + dcm_crawler = ps_io.SubjectDicomCrawler(dicom_subject_path, + modality_extractor=ExampleModalityExtractor()) + dicom_series_info = dcm_crawler.execute() + + # (optional) + # Keep all SeriesInfo entries that do not describe a DICOM-RTSS for loading + dicom_series_info = selection.execute(dicom_series_info) + + # (optional) + # Define the metadata for the DICOM-RTSS + # Note: For some attributes, the value must follow the value + # representation of the DICOM standard. + meta_data = ps_io.RTSSMetaData(patient_size='180', + patient_weight='80', + patient_age='050Y', + series_description='Converted from NIfTI') + + # Convert the segmentations to a DICOM-RTSS with standard smoothing settings. + # For the conversion we can either use a 2D or a 3D algorithm (see API reference + # for details). + # Note: Inappropriate smoothing leads to corrupted structures if their size + # is too small + if use_3d_conversion: + conv_conf = ps_io.RTSSConverter3DConfiguration() + else: + conv_conf = ps_io.RTSSConverter2DConfiguration() + + converter = ps_io.SubjectToRTSSConverter(subject, + dicom_series_info, + reference_modality, + conv_conf, + meta_data) + rtss = converter.convert() + + # Combine the DICOM-RTSS with its output file name + rtss_combination = ((rtss_filename, rtss),) + + # Write the DICOM-RTSS to a separate subject directory + # and include the DICOM files crawled before + # Note: If you want to output just a subset of the + # original DICOM files you may use additional selectors + writer.write(rtss_combination, output_dir_path, + subject.get_name(), dicom_series_info) + +########## + +def convert_to_rtss(dicom_series_path: str, # CT path + structure_masks, + rtss_file: str, + use_3d_conversion: bool = True + ) -> None: + # Specify a reference modalities + # This is the modality of the DICOM image series that will be + # referenced in the DICOM-RTSS. + reference_modality = 'T1' + + # Create the loader + loader = ps_io.SubjectLoader() + + # Create the writer and specify the output file name of the + # DICOM-RTSS files + writer = ps_io.DicomSeriesSubjectWriter() + rtss_filename = rtss_file + + # (optional) + # Instantiate a new selection to exclude the original DICOM-RTSS SeriesInfo + # Note: If this is omitted the original DICOM-RTSS will be copied to the + # corresponding output directory. + selection = ps_io.NoRTSSInfoSelector() + + # Create the file crawler for the discrete image files and + # loop through the subjects + crawler = ps_io.DatasetFileCrawler(input_dir_path, + extension='.nii.gz', + modality_extractor=ExampleModalityExtractor(), + organ_extractor=ExampleOrganExtractor(), + annotator_extractor=ExampleAnnotatorExtractor()) + for series_info in crawler: + # Load the subject + subject = loader.load(series_info) + + # Print the progress + print(f'Converting subject {subject.get_name()}...') + + # Construct the path to the subject's DICOM images + dicom_subject_path = os.path.join(dicom_image_dir_path, subject.get_name()) + + # Construct a DICOM crawler to retrieve the reference + # DICOM image series info + dcm_crawler = ps_io.SubjectDicomCrawler(dicom_subject_path, + modality_extractor=ExampleModalityExtractor()) + dicom_series_info = dcm_crawler.execute() + + # (optional) + # Keep all SeriesInfo entries that do not describe a DICOM-RTSS for loading + dicom_series_info = selection.execute(dicom_series_info) + + # (optional) + # Define the metadata for the DICOM-RTSS + # Note: For some attributes, the value must follow the value + # representation of the DICOM standard. + meta_data = ps_io.RTSSMetaData(patient_size='180', + patient_weight='80', + patient_age='050Y', + series_description='Converted from NIfTI') + + # Convert the segmentations to a DICOM-RTSS with standard smoothing settings. + # For the conversion we can either use a 2D or a 3D algorithm (see API reference + # for details). + # Note: Inappropriate smoothing leads to corrupted structures if their size + # is too small + if use_3d_conversion: + conv_conf = ps_io.RTSSConverter3DConfiguration() + else: + conv_conf = ps_io.RTSSConverter2DConfiguration() + + converter = ps_io.SubjectToRTSSConverter(subject, + dicom_series_info, + reference_modality, + conv_conf, + meta_data) + rtss = converter.convert() + + # Combine the DICOM-RTSS with its output file name + rtss_combination = ((rtss_filename, rtss),) + + # Write the DICOM-RTSS to a separate subject directory + # and include the DICOM files crawled before + # Note: If you want to output just a subset of the + # original DICOM files you may use additional selectors + writer.write(rtss_combination, output_dir_path, + subject.get_name(), dicom_series_info) + + +########## + +if __name__ == '__main__': + + # The indicator if the 2D or the 3D conversion algorithm should + # be used. + use_3d_algorithm = True + + # The input path pointing to the top-level directory containing the + # NIfTI subject directories + input_dataset_path = '//YOUR/PATH/TO/THE/EXAMPLE/DATA/nifti_data' + + # The input path pointing to the top-level directory containing the + # DICOM subject directories that will get referenced in the output + # DICOM-RTSS files + dicom_dataset_path = '//YOUR/PATH/TO/THE/EXAMPLE/DATA/dicom_data' + + # The output path pointing to an empty directory where the output + # will be saved + output_dataset_path = '//YOUR/PATH/TO/THE/OUTPUT/DIRECTORY/' + + # Execution of the conversion procedure + convert_subject_to_dicom_rtss(input_dataset_path, + output_dataset_path, + dicom_dataset_path, + use_3d_algorithm) diff --git a/onlylian/oar.py b/onlylian/oar.py new file mode 100755 index 0000000..63000b1 --- /dev/null +++ b/onlylian/oar.py @@ -0,0 +1,1121 @@ +# import os +# import shutil +# import subprocess +# import sys +# import time +# import logging +# from pathlib import Path +# from typing import Dict, List, Tuple + +# from nipype.interfaces.dcm2nii import Dcm2niix +# from pydicom import dcmread +# from pynetdicom import AE, debug_logger +# from pynetdicom.sop_class import CTImageStorage, RTStructureSetStorage +# from rt_utils import RTStructBuilder + +# import numpy as np +# import SimpleITK as sitk + +# from registration.best_reg import reg_transform + +# # 設定顏色映射 +# LABEL_COLORS: Dict[str, List[int]] = { +# "Brainstem": [0, 255, 0], # 綠色 +# "Right Eye": [255, 255, 224], # 淺黃色 +# "Left Eye": [139, 69, 19], # 棕色 +# "Optic Chiasm": [0, 0, 255], # 藍色 +# "Right_Optic_Nerve": [255, 127, 80], # 珊瑚紅 +# "Left_Optic_Nerve": [255, 127, 80], # 珊瑚紅 +# "TV": [255, 0, 0] # 紅色 +# } + +# LABEL_MAPPING: Dict[int, str] = { +# 1: "Brainstem", +# 2: "Right Eye", +# 3: "Left Eye", +# 4: "Optic Chiasm", +# 5: "Right_Optic_Nerve", +# 6: "Left_Optic_Nerve", +# 7: "TV" +# } + +# # 設定日誌 +# logging.basicConfig( +# level=logging.INFO, +# format='%(asctime)s - %(levelname)s - %(message)s', +# handlers=[ +# logging.StreamHandler(), +# logging.FileHandler('medical_imaging.log') +# ] +# ) +# logger = logging.getLogger(__name__) + +# def validate_dicom_directory(dicom_path): +# """加強版 DICOM 目錄驗證""" +# try: +# if not os.path.exists(dicom_path): +# return False, f"目錄不存在:{dicom_path}" + +# if not os.listdir(dicom_path): +# return False, f"目錄是空的:{dicom_path}" + +# dicom_files = [] +# non_dicom = [] +# corrupt_files = [] +# series_info = {} + +# for filename in os.listdir(dicom_path): +# filepath = os.path.join(dicom_path, filename) +# if os.path.isfile(filepath): +# try: +# ds = dcmread(filepath) +# if hasattr(ds, 'SeriesInstanceUID'): +# series_uid = ds.SeriesInstanceUID +# if series_uid not in series_info: +# series_info[series_uid] = [] +# series_info[series_uid].append(filepath) +# dicom_files.append(filepath) +# else: +# non_dicom.append(filename) +# except Exception as e: +# logger.warning(f"無法讀取檔案 {filename}: {str(e)}") +# if filename.endswith(('.dcm', '.DCM')): +# corrupt_files.append(filename) +# else: +# non_dicom.append(filename) + +# if not dicom_files: +# message = f"在 {dicom_path} 中找不到有效的 DICOM 檔案\n" +# if non_dicom: +# message += f"發現 {len(non_dicom)} 個非 DICOM 檔案\n" +# if corrupt_files: +# message += f"發現 {len(corrupt_files)} 個損壞的 DICOM 檔案" +# return False, message + +# if len(series_info) > 1: +# logger.warning(f"發現多個系列: {len(series_info)}") + +# for series_uid, files in series_info.items(): +# if len(files) < 5: +# logger.warning(f"系列 {series_uid} 的影像數量過少: {len(files)}") + +# return True, f"找到 {len(dicom_files)} 個有效的 DICOM 檔案,分屬 {len(series_info)} 個系列" + +# except Exception as e: +# return False, f"驗證過程發生錯誤:{str(e)}" + +# def enhanced_dcm2nii(source_dir, output_dir): +# """加強版的 dcm2nii 轉換函式""" +# try: +# is_valid, message = validate_dicom_directory(source_dir) +# if not is_valid: +# return False, message, [] + +# os.makedirs(output_dir, exist_ok=True) +# initial_files = set(os.listdir(output_dir)) + +# converter = Dcm2niix() +# converter.inputs.source_dir = source_dir +# converter.inputs.output_dir = output_dir +# converter.inputs.compress = 'y' +# converter.inputs.merge_imgs = True +# converter.inputs.single_file = True +# converter.inputs.verbose = True +# converter.terminal_output = 'stream' + +# logger.info(f"執行 DICOM 轉換:{source_dir}") +# try: +# result = converter.run() + +# if hasattr(result, 'runtime'): +# if hasattr(result.runtime, 'stdout'): +# logger.info(f"dcm2niix 輸出:\n{result.runtime.stdout}") +# if hasattr(result.runtime, 'stderr'): +# logger.warning(f"dcm2niix 錯誤:\n{result.runtime.stderr}") +# except Exception as run_error: +# logger.error(f"dcm2niix 執行錯誤:{str(run_error)}") + +# final_files = set(os.listdir(output_dir)) +# new_files = final_files - initial_files +# nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + +# if not nifti_files: +# logger.warning("使用備用轉換選項重試...") +# converter.inputs.merge_imgs = False +# converter.inputs.single_file = False +# try: +# result = converter.run() +# except Exception as retry_error: +# logger.error(f"備用轉換選項執行錯誤:{str(retry_error)}") + +# final_files = set(os.listdir(output_dir)) +# new_files = final_files - initial_files +# nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + +# if not nifti_files: +# return False, "轉換完成但未產生 NIfTI 檔案", [] + +# valid_nifti = [] +# for nii_file in nifti_files: +# try: +# img = sitk.ReadImage(os.path.join(output_dir, nii_file)) +# if img.GetSize()[2] > 1: +# valid_nifti.append(nii_file) +# else: +# logger.warning(f"檔案 {nii_file} 不是有效的3D影像") +# except Exception as e: +# logger.warning(f"無法讀取 NIfTI 檔案 {nii_file}: {str(e)}") + +# if not valid_nifti: +# return False, "轉換產生的 NIfTI 檔案無效", [] + +# return True, f"成功轉換 {len(valid_nifti)} 個有效的 NIfTI 檔案", valid_nifti + +# except Exception as e: +# logger.error(f"DICOM 轉換錯誤:{str(e)}", exc_info=True) +# return False, f"轉換過程發生錯誤:{str(e)}", [] + +# def process_symmetric_structures(image_array): +# """處理對稱結構的後處理函數""" +# logger.info("開始處理對稱結構") +# result_img = image_array.copy() + +# # 定義需要處理的結構及其標籤 +# structures = [ +# {'name': '眼睛', 'labels': (2, 3), 'kernel_radius': 2}, +# {'name': '視神經', 'labels': (5, 6), 'kernel_radius': 2} +# ] + +# # 處理每個對稱結構 +# for structure in structures: +# logger.info(f"處理{structure['name']}結構") +# # 合併左右標籤 +# combined_mask = np.zeros_like(result_img, dtype=np.uint8) +# for label in structure['labels']: +# combined_mask |= (result_img == label) + +# # 形態學閉運算 +# combined_mask_sitk = sitk.GetImageFromArray(combined_mask) +# closing_filter = sitk.BinaryMorphologicalClosingImageFilter() +# closing_filter.SetKernelRadius(structure['kernel_radius']) +# processed_mask = sitk.GetArrayFromImage(closing_filter.Execute(combined_mask_sitk)) + +# # 連通區域標記 +# labeled_components = sitk.GetArrayFromImage( +# sitk.ConnectedComponent(sitk.GetImageFromArray(processed_mask)) +# ) + +# # 找出最大的兩個區域 +# unique, counts = np.unique(labeled_components, return_counts=True) +# if len(unique) >= 3: # 確保至少有兩個區域(除了背景) +# region_sizes = [(i, count) for i, count in zip(unique[1:], counts[1:])] +# sorted_regions = sorted(region_sizes, key=lambda x: x[1], reverse=True) + +# # 獲取最大的兩個區域 +# region1 = (labeled_components == sorted_regions[0][0]) +# region2 = (labeled_components == sorted_regions[1][0]) + +# # 根據質心的x座標決定左右 +# center1 = np.mean(np.where(region1)[2]) +# center2 = np.mean(np.where(region2)[2]) + +# # 分配左右標籤 +# if center1 > center2: +# right, left = region1, region2 +# else: +# right, left = region2, region1 + +# # 更新結果 +# result_img[right] = structure['labels'][0] # 右側標籤 +# result_img[left] = structure['labels'][1] # 左側標籤 + +# logger.info(f"完成{structure['name']}的左右結構處理") +# else: +# logger.warning(f"無法找到足夠的{structure['name']}區域進行處理") + +# return result_img + +# def create_structure_masks(image: sitk.Image) -> Dict[str, np.ndarray]: +# """從分割結果創建每個結構的遮罩""" +# array = sitk.GetArrayFromImage(image) +# masks = {} + +# for label_id, structure_name in LABEL_MAPPING.items(): +# mask = (array == label_id) +# if mask.any(): +# masks[structure_name] = np.transpose(mask, (1, 2, 0)) + +# return masks + +# def inference(DCM_CT, DCM_MR): +# """執行影像推論和後處理""" +# logger.info(f"處理影像\nCT: {DCM_CT}\nMR: {DCM_MR}") + +# try: +# # 驗證輸入目錄 +# for path, desc in [(DCM_CT, "CT"), (DCM_MR, "MR")]: +# is_valid, message = validate_dicom_directory(path) +# if not is_valid: +# raise ValueError(f"{desc} 影像驗證失敗:{message}") + +# # 設定工作目錄 +# ROOT_DIR = os.path.dirname(os.path.dirname(DCM_CT)) +# NII_DIR = os.path.join(ROOT_DIR, 'nii') +# INPUT_DIR = os.path.join(ROOT_DIR, 'input') +# OUTPUT_DIR = os.path.join(ROOT_DIR, 'output') + +# # 設定 RTSTRUCT 輸出路徑 +# head, tail = os.path.split(DCM_CT) +# rtss_file = os.path.join(head, tail+'-rtss.dcm') + +# # 清理並建立工作目錄 +# for dir_path in [NII_DIR, INPUT_DIR, OUTPUT_DIR]: +# shutil.rmtree(dir_path, ignore_errors=True) +# os.makedirs(dir_path, exist_ok=True) + +# # 取得基本檔名 +# nCT = os.path.basename(DCM_CT) +# nMR = os.path.basename(DCM_MR) + +# # DICOM 轉 NIfTI +# logger.info("開始轉換 CT DICOM...") +# success_ct, message_ct, ct_files = enhanced_dcm2nii(DCM_CT, NII_DIR) +# if not success_ct: +# raise RuntimeError(f"CT 轉換失敗:{message_ct}") + +# logger.info("開始轉換 MR DICOM...") +# success_mr, message_mr, mr_files = enhanced_dcm2nii(DCM_MR, NII_DIR) +# if not success_mr: +# raise RuntimeError(f"MR 轉換失敗:{message_mr}") + +# # 尋找轉換後的檔案 +# NII_CT = None +# NII_MR = None +# for f in os.listdir(NII_DIR): +# if f.endswith('.nii.gz'): +# full_path = os.path.join(NII_DIR, f) +# if f.startswith(nCT+'_'): +# NII_CT = full_path +# logger.info(f"找到 CT NIfTI 檔案:{f}") +# elif f.startswith(nMR+'_'): +# NII_MR = full_path +# logger.info(f"找到 MR NIfTI 檔案:{f}") + +# if not NII_CT: +# raise FileNotFoundError(f"找不到 CT 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") +# if not NII_MR: +# raise FileNotFoundError(f"找不到 MR 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + +# # 準備輸入檔案 +# basename = os.path.basename(NII_MR) +# old = '_'+basename.split('_')[-1] +# input_file = os.path.join(INPUT_DIR, basename.replace(old, '_0000.nii.gz')) +# output_file = os.path.join(OUTPUT_DIR, basename.replace(old, '.nii.gz')) + +# basename_ct = os.path.basename(NII_CT) +# old_ct = '_'+basename_ct.split('_')[-1] +# label_file = os.path.join(NII_DIR, basename_ct.replace(old_ct, '.label.nii.gz')) + +# logger.info(f"複製 MR NIfTI 到輸入目錄:{input_file}") +# shutil.copy(NII_MR, input_file) + +# logger.info("準備執行模型推論...") + +# # 設定 nnUNet 環境變數 +# model_dir = "/123/onlylian" +# os.environ['nnUNet_raw'] = "/123/onlylian/nnUNet_raw" +# os.environ['nnUNet_preprocessed'] = "/123/onlylian/nnUNet_preprocessed" +# os.environ['nnUNet_results'] = "/123/onlylian/nnUNet_results" + +# # 明確設定可用的資料集路徑 +# dataset_dir = os.path.join(model_dir, "nnUNet_results", "Dataset505") +# logger.info(f"使用資料集目錄:{dataset_dir}") + +# if not os.path.exists(dataset_dir): +# logger.error(f"資料集目錄不存在:{dataset_dir}") +# raise FileNotFoundError(f"找不到資料集目錄:{dataset_dir}") + +# try: +# # 執行模型推論 +# predict_cmd = [ +# "nnUNetv2_predict", +# "-i", INPUT_DIR, +# "-o", OUTPUT_DIR, +# "-d", "505", +# "-c", "3d_fullres", +# "-f", "0", +# "-tr", "nnUNetTrainer", +# "-p", "nnUNetPlans" +# ] + +# logger.info(f"執行推論指令:{' '.join(predict_cmd)}") +# result = subprocess.run( +# predict_cmd, +# check=True, +# capture_output=True, +# text=True, +# env=os.environ +# ) + +# logger.info(f"模型推論輸出:\n{result.stdout}") + +# except subprocess.CalledProcessError as e: +# logger.error(f"模型推論失敗:\n輸出:{e.output}\n錯誤:{e.stderr}") +# raise RuntimeError(f"模型推論失敗:{e.stderr}") + +# if not os.path.exists(output_file): +# raise FileNotFoundError(f"找不到模型輸出檔案:{output_file}") + +# logger.info("開始執行影像後處理...") + +# try: +# # 讀取預測結果 +# predicted_image = sitk.ReadImage(output_file) +# predicted_array = sitk.GetArrayFromImage(predicted_image) + +# # 執行後處理 +# processed_array = process_symmetric_structures(predicted_array) +# processed_image = sitk.GetImageFromArray(processed_array) +# processed_image.CopyInformation(predicted_image) + +# # 暫時保存後處理結果 +# processed_output = os.path.join(OUTPUT_DIR, 'processed_' + os.path.basename(output_file)) +# sitk.WriteImage(processed_image, processed_output) +# logger.info(f"後處理結果已保存至:{processed_output}") + +# logger.info("開始執行影像配準...") +# reg_transform(NII_CT, NII_MR, processed_output, label_file) + +# except Exception as e: +# logger.error(f"影像處理失敗:{str(e)}") +# raise RuntimeError("後處理或配準失敗") + +# if not os.path.exists(label_file): +# raise FileNotFoundError(f"找不到配準後的標籤檔案:{label_file}") + +# logger.info("開始建立 RTSTRUCT...") + +# # 讀取 CT 影像序列 +# try: +# reader = sitk.ImageSeriesReader() +# dicom_names = reader.GetGDCMSeriesFileNames(DCM_CT) +# reader.SetFileNames(dicom_names) +# reader.MetaDataDictionaryArrayUpdateOn() +# reader.LoadPrivateTagsOn() +# image = reader.Execute() +# except Exception as e: +# logger.error(f"讀取 CT 系列失敗:{str(e)}") +# raise RuntimeError("無法讀取 CT 影像系列") + +# # 讀取並重新取樣配準後的預測結果 +# try: +# final_image = sitk.ReadImage(label_file) +# final_image = sitk.Resample( +# final_image, +# image, +# sitk.Transform(), +# sitk.sitkNearestNeighbor +# ) +# except Exception as e: +# logger.error(f"重新取樣失敗:{str(e)}") +# raise RuntimeError("預測結果重新取樣失敗") + +# # 建立 RTSTRUCT +# try: +# rtstruct = RTStructBuilder.create_new(dicom_series_path=DCM_CT) + +# # 為每個解剖結構創建遮罩 +# structure_masks = create_structure_masks(final_image) + +# # 確認是否有找到任何結構 +# if not structure_masks: +# logger.warning("未找到任何有效的結構遮罩") + +# # 添加每個結構到 RTSTRUCT +# for structure_name, mask in structure_masks.items(): +# if mask.any(): # 確保遮罩不是空的 +# logger.info(f"添加結構 {structure_name}") +# try: +# rtstruct.add_roi( +# mask=mask, +# color=LABEL_COLORS[structure_name], +# name=structure_name +# ) +# logger.info(f"成功添加 {structure_name}") +# except Exception as roi_error: +# logger.error(f"添加 ROI {structure_name} 時發生錯誤: {str(roi_error)}") +# continue + +# logger.info(f"儲存 RTSTRUCT:{rtss_file}") +# rtstruct.save(rtss_file) + +# except Exception as e: +# logger.error(f"RTSTRUCT 生成失敗:{str(e)}") +# raise RuntimeError("無法生成或儲存 RTSTRUCT") + +# return rtss_file + +# except Exception as e: +# logger.error(f"推論過程發生錯誤:{str(e)}", exc_info=True) +# raise + +# def SendDCM(fp): +# """傳送 DICOM 檔案到 PACS""" +# logger.info(f"準備傳送 DICOM 檔案:{fp}") +# debug_logger() + +# if not os.path.exists(fp): +# raise FileNotFoundError(f"找不到 DICOM 檔案:{fp}") + +# try: +# ae = AE() +# ae.ae_title = 'OUR_STORE_SCP' +# ae.add_requested_context(RTStructureSetStorage) + +# try: +# ds = dcmread(fp) +# except Exception as e: +# logger.error(f"讀取 DICOM 檔案失敗:{str(e)}") +# raise RuntimeError("無法讀取 DICOM 檔案") + +# logger.info("正在連接 PACS 伺服器...") +# try: +# assoc = ae.associate("172.16.40.36", 104, ae_title='N1000_STORAGE') +# except Exception as e: +# logger.error(f"PACS 連接失敗:{str(e)}") +# raise RuntimeError("無法連接 PACS 伺服器") + +# if assoc.is_established: +# try: +# status = assoc.send_c_store(ds) +# if status: +# logger.info(f'DICOM 傳送成功,狀態碼: 0x{status.Status:04x}') +# else: +# raise RuntimeError('傳送失敗:連接逾時或收到無效回應') +# except Exception as e: +# logger.error(f"DICOM 傳送失敗:{str(e)}") +# raise +# finally: +# assoc.release() +# else: +# raise RuntimeError('傳送失敗:無法建立連接') + +# except Exception as e: +# logger.error(f"DICOM 傳送過程發生錯誤:{str(e)}", exc_info=True) +# raise + +# def main(): +# """主程式進入點""" +# if len(sys.argv) < 3: +# print('使用方式:', sys.argv[0], ' ') +# print('範例: python oar.py /nn/3894670/20241111/CT/a /nn/3894670/20241111/MR/6') +# sys.exit(1) + +# logger.info('開始執行') +# logger.info('程式名稱: %s', sys.argv[0]) +# logger.info('CT路徑: %s', sys.argv[1]) +# logger.info('MR路徑: %s', sys.argv[2]) + +# start_time = time.time() +# try: +# rtss_file = inference(sys.argv[1], sys.argv[2]) +# SendDCM(rtss_file) +# logger.info(f"處理完成,總耗時:{time.time() - start_time:.2f} 秒") +# except Exception as e: +# logger.error(f"處理過程發生錯誤:{str(e)}", exc_info=True) +# sys.exit(1) + +# if __name__ == '__main__': +# main() + + +import os +import shutil +import subprocess +import sys +import time +import logging +import colorsys +from pathlib import Path +from typing import Dict, List, Tuple + +from nipype.interfaces.dcm2nii import Dcm2niix +from pydicom import dcmread +from pynetdicom import AE, debug_logger +from pynetdicom.sop_class import CTImageStorage, RTStructureSetStorage +from rt_utils import RTStructBuilder + +import numpy as np +import SimpleITK as sitk + +from registration.best_reg import reg_transform + +def generate_distinct_colors(n): + """生成 n 個視覺上區分度高的顏色""" + colors = [] + for i in range(n): + # 使用黃金比例來產生分散的色相值 + hue = (i * 0.618033988749895) % 1 + # 使用較高的飽和度和亮度以確保顏色明顯 + saturation = 0.7 + (i % 3) * 0.1 # 0.7-0.9 + value = 0.8 + (i % 2) * 0.1 # 0.8-0.9 + + # 轉換 HSV 到 RGB + rgb = colorsys.hsv_to_rgb(hue, saturation, value) + # 轉換到 0-255 範圍 + color = [int(x * 255) for x in rgb] + colors.append(color) + + return colors + +# 設定顏色映射 +LABEL_COLORS: Dict[str, List[int]] = { + "Brainstem": [0, 255, 0], # 綠色 + "Right Eye": [255, 255, 224], # 淺黃色 + "Left Eye": [255, 255, 224], # 淺黃色 + "Optic Chiasm": [0, 0, 255], # 藍色 + "Right_Optic_Nerve": [255, 127, 80], # 珊瑚紅 + "Left_Optic_Nerve": [255, 127, 80] # 珊瑚紅 +} + +# 設定標籤映射 +LABEL_MAPPING: Dict[int, str] = { + 1: "Brainstem", + 2: "Right Eye", + 3: "Left Eye", + 4: "Optic Chiasm", + 5: "Right_Optic_Nerve", + 6: "Left_Optic_Nerve" +} + +# 設定日誌 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('medical_imaging.log') + ] +) +logger = logging.getLogger(__name__) + +def validate_dicom_directory(dicom_path): + """加強版 DICOM 目錄驗證""" + try: + if not os.path.exists(dicom_path): + return False, f"目錄不存在:{dicom_path}" + + if not os.listdir(dicom_path): + return False, f"目錄是空的:{dicom_path}" + + dicom_files = [] + non_dicom = [] + corrupt_files = [] + series_info = {} + + for filename in os.listdir(dicom_path): + filepath = os.path.join(dicom_path, filename) + if os.path.isfile(filepath): + try: + ds = dcmread(filepath) + if hasattr(ds, 'SeriesInstanceUID'): + series_uid = ds.SeriesInstanceUID + if series_uid not in series_info: + series_info[series_uid] = [] + series_info[series_uid].append(filepath) + dicom_files.append(filepath) + else: + non_dicom.append(filename) + except Exception as e: + logger.warning(f"無法讀取檔案 {filename}: {str(e)}") + if filename.endswith(('.dcm', '.DCM')): + corrupt_files.append(filename) + else: + non_dicom.append(filename) + + if not dicom_files: + message = f"在 {dicom_path} 中找不到有效的 DICOM 檔案\n" + if non_dicom: + message += f"發現 {len(non_dicom)} 個非 DICOM 檔案\n" + if corrupt_files: + message += f"發現 {len(corrupt_files)} 個損壞的 DICOM 檔案" + return False, message + + if len(series_info) > 1: + logger.warning(f"發現多個系列: {len(series_info)}") + + for series_uid, files in series_info.items(): + if len(files) < 5: + logger.warning(f"系列 {series_uid} 的影像數量過少: {len(files)}") + + return True, f"找到 {len(dicom_files)} 個有效的 DICOM 檔案,分屬 {len(series_info)} 個系列" + + except Exception as e: + return False, f"驗證過程發生錯誤:{str(e)}" + +def enhanced_dcm2nii(source_dir, output_dir): + """加強版的 dcm2nii 轉換函式""" + try: + is_valid, message = validate_dicom_directory(source_dir) + if not is_valid: + return False, message, [] + + os.makedirs(output_dir, exist_ok=True) + initial_files = set(os.listdir(output_dir)) + + converter = Dcm2niix() + converter.inputs.source_dir = source_dir + converter.inputs.output_dir = output_dir + converter.inputs.compress = 'y' + converter.inputs.merge_imgs = True + converter.inputs.single_file = True + converter.inputs.verbose = True + converter.terminal_output = 'stream' + + logger.info(f"執行 DICOM 轉換:{source_dir}") + try: + result = converter.run() + + if hasattr(result, 'runtime'): + if hasattr(result.runtime, 'stdout'): + logger.info(f"dcm2niix 輸出:\n{result.runtime.stdout}") + if hasattr(result.runtime, 'stderr'): + logger.warning(f"dcm2niix 錯誤:\n{result.runtime.stderr}") + except Exception as run_error: + logger.error(f"dcm2niix 執行錯誤:{str(run_error)}") + + final_files = set(os.listdir(output_dir)) + new_files = final_files - initial_files + nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + + if not nifti_files: + logger.warning("使用備用轉換選項重試...") + converter.inputs.merge_imgs = False + converter.inputs.single_file = False + try: + result = converter.run() + except Exception as retry_error: + logger.error(f"備用轉換選項執行錯誤:{str(retry_error)}") + + final_files = set(os.listdir(output_dir)) + new_files = final_files - initial_files + nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + + if not nifti_files: + return False, "轉換完成但未產生 NIfTI 檔案", [] + + valid_nifti = [] + for nii_file in nifti_files: + try: + img = sitk.ReadImage(os.path.join(output_dir, nii_file)) + if img.GetSize()[2] > 1: + valid_nifti.append(nii_file) + else: + logger.warning(f"檔案 {nii_file} 不是有效的3D影像") + except Exception as e: + logger.warning(f"無法讀取 NIfTI 檔案 {nii_file}: {str(e)}") + + if not valid_nifti: + return False, "轉換產生的 NIfTI 檔案無效", [] + + return True, f"成功轉換 {len(valid_nifti)} 個有效的 NIfTI 檔案", valid_nifti + + except Exception as e: + logger.error(f"DICOM 轉換錯誤:{str(e)}", exc_info=True) + return False, f"轉換過程發生錯誤:{str(e)}", [] + +def process_symmetric_structures(image_array): + """處理對稱結構的後處理函數""" + logger.info("開始處理對稱結構") + result_img = image_array.copy() + + # 定義需要處理的結構及其標籤 + structures = [ + {'name': '眼睛', 'labels': (2, 3), 'kernel_radius': 2}, + {'name': '視神經', 'labels': (5, 6), 'kernel_radius': 2} + ] + + # 處理每個對稱結構 + for structure in structures: + logger.info(f"處理{structure['name']}結構") + # 合併左右標籤 + combined_mask = np.zeros_like(result_img, dtype=np.uint8) + for label in structure['labels']: + combined_mask |= (result_img == label) + + # 形態學閉運算 + combined_mask_sitk = sitk.GetImageFromArray(combined_mask) + closing_filter = sitk.BinaryMorphologicalClosingImageFilter() + closing_filter.SetKernelRadius(structure['kernel_radius']) + processed_mask = sitk.GetArrayFromImage(closing_filter.Execute(combined_mask_sitk)) + + # 連通區域標記 + labeled_components = sitk.GetArrayFromImage( + sitk.ConnectedComponent(sitk.GetImageFromArray(processed_mask)) + ) + + # 找出最大的兩個區域 + unique, counts = np.unique(labeled_components, return_counts=True) + if len(unique) >= 3: # 確保至少有兩個區域(除了背景) + region_sizes = [(i, count) for i, count in zip(unique[1:], counts[1:])] + sorted_regions = sorted(region_sizes, key=lambda x: x[1], reverse=True) + + # 獲取最大的兩個區域 + region1 = (labeled_components == sorted_regions[0][0]) + region2 = (labeled_components == sorted_regions[1][0]) + + # 根據質心的x座標決定左右 + center1 = np.mean(np.where(region1)[2]) + center2 = np.mean(np.where(region2)[2]) + + # 分配左右標籤 + if center1 > center2: + right, left = region1, region2 + else: + right, left = region2, region1 + + # 更新結果 + result_img[right] = structure['labels'][0] # 右側標籤 + result_img[left] = structure['labels'][1] # 左側標籤 + + logger.info(f"完成{structure['name']}的左右結構處理") + else: + logger.warning(f"無法找到足夠的{structure['name']}區域進行處理") + + return result_img + +def process_multiple_tumors(image_array): + """處理多個腫瘤的函數""" + logger.info("開始處理多個腫瘤") + result_img = image_array.copy() + + # 假設原始標記中 7 代表腫瘤 + tumor_mask = (result_img == 7) + if not tumor_mask.any(): + logger.warning("未找到腫瘤區域") + return result_img + + # 使用連通區域分析找出不同的腫瘤 + tumor_mask_sitk = sitk.GetImageFromArray(tumor_mask.astype(np.uint8)) + connected_filter = sitk.ConnectedComponentImageFilter() + connected_filter.FullyConnectedOn() + labeled_tumors = sitk.GetArrayFromImage(connected_filter.Execute(tumor_mask_sitk)) + + num_tumors = connected_filter.GetObjectCount() + logger.info(f"找到 {num_tumors} 個腫瘤") + + if num_tumors == 0: + return result_img + + # 為每個腫瘤生成唯一的標籤值,從最大現有標籤值開始 + max_existing_label = max(LABEL_MAPPING.keys()) + tumor_colors = generate_distinct_colors(num_tumors) + + # 更新標籤映射和顏色映射 + for i in range(num_tumors): + tumor_label = max_existing_label + i + 1 + tumor_name = f"TV_{i+1}" + LABEL_MAPPING[tumor_label] = tumor_name + LABEL_COLORS[tumor_name] = tumor_colors[i] + + # 更新結果圖像中的腫瘤標籤 + result_img[labeled_tumors == (i + 1)] = tumor_label + logger.info(f"標記腫瘤 {tumor_name} 使用標籤 {tumor_label} 和顏色 {tumor_colors[i]}") + + return result_img + +def create_structure_masks(image: sitk.Image) -> Dict[str, np.ndarray]: + """從分割結果創建每個結構的遮罩""" + array = sitk.GetArrayFromImage(image) + masks = {} + + for label_id, structure_name in LABEL_MAPPING.items(): + mask = (array == label_id) + if mask.any(): + masks[structure_name] = np.transpose(mask, (1, 2, 0)) + + return masks + +def inference(DCM_CT, DCM_MR): + """執行影像推論和後處理""" + logger.info(f"處理影像\nCT: {DCM_CT}\nMR: {DCM_MR}") + + try: + # 驗證# 驗證輸入目錄 + for path, desc in [(DCM_CT, "CT"), (DCM_MR, "MR")]: + is_valid, message = validate_dicom_directory(path) + if not is_valid: + raise ValueError(f"{desc} 影像驗證失敗:{message}") + + # 設定工作目錄 + ROOT_DIR = os.path.dirname(os.path.dirname(DCM_CT)) + NII_DIR = os.path.join(ROOT_DIR, 'nii') + INPUT_DIR = os.path.join(ROOT_DIR, 'input') + OUTPUT_DIR = os.path.join(ROOT_DIR, 'output') + + # 設定 RTSTRUCT 輸出路徑 + head, tail = os.path.split(DCM_CT) + rtss_file = os.path.join(head, tail+'-rtss.dcm') + + # 清理並建立工作目錄 + for dir_path in [NII_DIR, INPUT_DIR, OUTPUT_DIR]: + shutil.rmtree(dir_path, ignore_errors=True) + os.makedirs(dir_path, exist_ok=True) + + # 取得基本檔名 + nCT = os.path.basename(DCM_CT) + nMR = os.path.basename(DCM_MR) + + # DICOM 轉 NIfTI + logger.info("開始轉換 CT DICOM...") + success_ct, message_ct, ct_files = enhanced_dcm2nii(DCM_CT, NII_DIR) + if not success_ct: + raise RuntimeError(f"CT 轉換失敗:{message_ct}") + + logger.info("開始轉換 MR DICOM...") + success_mr, message_mr, mr_files = enhanced_dcm2nii(DCM_MR, NII_DIR) + if not success_mr: + raise RuntimeError(f"MR 轉換失敗:{message_mr}") + + # 尋找轉換後的檔案 + NII_CT = None + NII_MR = None + for f in os.listdir(NII_DIR): + if f.endswith('.nii.gz'): + full_path = os.path.join(NII_DIR, f) + if f.startswith(nCT+'_'): + NII_CT = full_path + logger.info(f"找到 CT NIfTI 檔案:{f}") + elif f.startswith(nMR+'_'): + NII_MR = full_path + logger.info(f"找到 MR NIfTI 檔案:{f}") + + if not NII_CT: + raise FileNotFoundError(f"找不到 CT 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + if not NII_MR: + raise FileNotFoundError(f"找不到 MR 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + + # 準備輸入檔案 + basename = os.path.basename(NII_MR) + old = '_'+basename.split('_')[-1] + input_file = os.path.join(INPUT_DIR, basename.replace(old, '_0000.nii.gz')) + output_file = os.path.join(OUTPUT_DIR, basename.replace(old, '.nii.gz')) + + basename_ct = os.path.basename(NII_CT) + old_ct = '_'+basename_ct.split('_')[-1] + label_file = os.path.join(NII_DIR, basename_ct.replace(old_ct, '.label.nii.gz')) + + logger.info(f"複製 MR NIfTI 到輸入目錄:{input_file}") + shutil.copy(NII_MR, input_file) + + logger.info("準備執行模型推論...") + + # 設定 nnUNet 環境變數 + model_dir = "/123/onlylian" + os.environ['nnUNet_raw'] = "/123/onlylian/nnUNet_raw" + os.environ['nnUNet_preprocessed'] = "/123/onlylian/nnUNet_preprocessed" + os.environ['nnUNet_results'] = "/123/onlylian/nnUNet_results" + + # 明確設定可用的資料集路徑 + dataset_dir = os.path.join(model_dir, "nnUNet_results", "Dataset505") + logger.info(f"使用資料集目錄:{dataset_dir}") + + if not os.path.exists(dataset_dir): + logger.error(f"資料集目錄不存在:{dataset_dir}") + raise FileNotFoundError(f"找不到資料集目錄:{dataset_dir}") + + try: + # 執行模型推論 + predict_cmd = [ + "nnUNetv2_predict", + "-i", INPUT_DIR, + "-o", OUTPUT_DIR, + "-d", "505", + "-c", "3d_fullres", + "-f", "0", + "-tr", "nnUNetTrainer", + "-p", "nnUNetPlans" + ] + + logger.info(f"執行推論指令:{' '.join(predict_cmd)}") + result = subprocess.run( + predict_cmd, + check=True, + capture_output=True, + text=True, + env=os.environ + ) + + logger.info(f"模型推論輸出:\n{result.stdout}") + + except subprocess.CalledProcessError as e: + logger.error(f"模型推論失敗:\n輸出:{e.output}\n錯誤:{e.stderr}") + raise RuntimeError(f"模型推論失敗:{e.stderr}") + + if not os.path.exists(output_file): + raise FileNotFoundError(f"找不到模型輸出檔案:{output_file}") + + logger.info("開始執行影像後處理...") + + try: + # 讀取預測結果 + predicted_image = sitk.ReadImage(output_file) + predicted_array = sitk.GetArrayFromImage(predicted_image) + + # 執行後處理 + processed_array = process_symmetric_structures(predicted_array) + processed_array = process_multiple_tumors(processed_array) + + processed_image = sitk.GetImageFromArray(processed_array) + processed_image.CopyInformation(predicted_image) + + # 暫時保存後處理結果 + processed_output = os.path.join(OUTPUT_DIR, 'processed_' + os.path.basename(output_file)) + sitk.WriteImage(processed_image, processed_output) + logger.info(f"後處理結果已保存至:{processed_output}") + + logger.info("開始執行影像配準...") + reg_transform(NII_CT, NII_MR, processed_output, label_file) + + except Exception as e: + logger.error(f"影像處理失敗:{str(e)}") + raise RuntimeError("後處理或配準失敗") + + if not os.path.exists(label_file): + raise FileNotFoundError(f"找不到配準後的標籤檔案:{label_file}") + + logger.info("開始建立 RTSTRUCT...") + + # 讀取 CT 影像序列 + try: + reader = sitk.ImageSeriesReader() + dicom_names = reader.GetGDCMSeriesFileNames(DCM_CT) + reader.SetFileNames(dicom_names) + reader.MetaDataDictionaryArrayUpdateOn() + reader.LoadPrivateTagsOn() + image = reader.Execute() + except Exception as e: + logger.error(f"讀取 CT 系列失敗:{str(e)}") + raise RuntimeError("無法讀取 CT 影像系列") + + # 讀取並重新取樣配準後的預測結果 + try: + final_image = sitk.ReadImage(label_file) + final_image = sitk.Resample( + final_image, + image, + sitk.Transform(), + sitk.sitkNearestNeighbor + ) + except Exception as e: + logger.error(f"重新取樣失敗:{str(e)}") + raise RuntimeError("預測結果重新取樣失敗") + + # 建立 RTSTRUCT + try: + rtstruct = RTStructBuilder.create_new(dicom_series_path=DCM_CT) + + # 為每個解剖結構創建遮罩 + structure_masks = create_structure_masks(final_image) + + # 確認是否有找到任何結構 + if not structure_masks: + logger.warning("未找到任何有效的結構遮罩") + + # 添加每個結構到 RTSTRUCT + for structure_name, mask in structure_masks.items(): + if mask.any(): # 確保遮罩不是空的 + logger.info(f"添加結構 {structure_name}") + try: + rtstruct.add_roi( + mask=mask, + color=LABEL_COLORS[structure_name], + name=structure_name + ) + logger.info(f"成功添加 {structure_name}") + except Exception as roi_error: + logger.error(f"添加 ROI {structure_name} 時發生錯誤: {str(roi_error)}") + continue + + logger.info(f"儲存 RTSTRUCT:{rtss_file}") + rtstruct.save(rtss_file) + + except Exception as e: + logger.error(f"RTSTRUCT 生成失敗:{str(e)}") + raise RuntimeError("無法生成或儲存 RTSTRUCT") + + return rtss_file + + except Exception as e: + logger.error(f"推論過程發生錯誤:{str(e)}", exc_info=True) + raise + +def SendDCM(fp): + """傳送 DICOM 檔案到 PACS""" + logger.info(f"準備傳送 DICOM 檔案:{fp}") + debug_logger() + + if not os.path.exists(fp): + raise FileNotFoundError(f"找不到 DICOM 檔案:{fp}") + + try: + ae = AE() + ae.ae_title = 'OUR_STORE_SCP' + ae.add_requested_context(RTStructureSetStorage) + + try: + ds = dcmread(fp) + except Exception as e: + logger.error(f"讀取 DICOM 檔案失敗:{str(e)}") + raise RuntimeError("無法讀取 DICOM 檔案") + + logger.info("正在連接 PACS 伺服器...") + try: + assoc = ae.associate("172.16.40.36", 104, ae_title='N1000_STORAGE') + except Exception as e: + logger.error(f"PACS 連接失敗:{str(e)}") + raise RuntimeError("無法連接 PACS 伺服器") + + if assoc.is_established: + try: + status = assoc.send_c_store(ds) + if status: + logger.info(f'DICOM 傳送成功,狀態碼: 0x{status.Status:04x}') + else: + raise RuntimeError('傳送失敗:連接逾時或收到無效回應') + except Exception as e: + logger.error(f"DICOM 傳送失敗:{str(e)}") + raise + finally: + assoc.release() + else: + raise RuntimeError('傳送失敗:無法建立連接') + + except Exception as e: + logger.error(f"DICOM 傳送過程發生錯誤:{str(e)}", exc_info=True) + raise + +def main(): + """主程式進入點""" + if len(sys.argv) < 3: + print('使用方式:', sys.argv[0], ' ') + print('範例: python oar.py /nn/3894670/20241111/CT/a /nn/3894670/20241111/MR/6') + sys.exit(1) + + logger.info('開始執行') + logger.info('程式名稱: %s', sys.argv[0]) + logger.info('CT路徑: %s', sys.argv[1]) + logger.info('MR路徑: %s', sys.argv[2]) + + start_time = time.time() + try: + rtss_file = inference(sys.argv[1], sys.argv[2]) + SendDCM(rtss_file) + logger.info(f"處理完成,總耗時:{time.time() - start_time:.2f} 秒") + except Exception as e: + logger.error(f"處理過程發生錯誤:{str(e)}", exc_info=True) + sys.exit(1) + +if __name__ == '__main__': + main() + \ No newline at end of file diff --git a/onlylian/oar2.py b/onlylian/oar2.py new file mode 100755 index 0000000..9b5a74d --- /dev/null +++ b/onlylian/oar2.py @@ -0,0 +1,707 @@ + +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import os +import shutil +import subprocess +import sys +import time +import logging +import colorsys + +from nipype.interfaces.dcm2nii import Dcm2niix +from pydicom import dcmread +from pynetdicom import AE, debug_logger +from pynetdicom.sop_class import CTImageStorage, RTStructureSetStorage +from rt_utils import RTStructBuilder + +import numpy as np +import SimpleITK as sitk + +from pyradise.data import (Subject, IntensityImage, SegmentationImage, + Modality, Organ, Annotator) +from pyradise.fileio.extraction import SimpleModalityExtractor +import pyradise.data as ps_data +import pyradise.fileio as ps_io + +from registration.best_reg import reg_transform + +class ExampleModalityExtractor(ps_io.ModalityExtractor): + + def extract_from_dicom(self, + path: str + ) -> Optional[ps_data.Modality]: + # Extract the necessary attributes from the DICOM file + tags = (ps_io.Tag((0x0008, 0x0060)), # Modality + ps_io.Tag((0x0008, 0x103e))) # Series Description + dataset_dict = self._load_dicom_attributes(tags, path) + + # Identify the modality rule-based + modality = dataset_dict.get('Modality', {}).get('value', None) + series_desc = dataset_dict.get('Series Description', {}).get('value', '') + if modality == 'MR': + if 't1' in series_desc.lower(): + return ps_data.Modality('T1') + elif 't2' in series_desc.lower(): + return ps_data.Modality('T2') + else: + return None + elif modality == 'CT': + return ps_data.Modality('CT') + else: + return None + + def extract_from_path(self, + path: str + ) -> Optional[ps_data.Modality]: + + if 'CT' in path: + return ps_data.Modality('CT') + + # Identify the discrete image file's modality rule-based + filename = os.path.basename(path) + + # Check if the image contains an img prefix + # (i.e., it is a intensity image) + if not filename.startswith('img'): + return None + + # Check if the image contains a modality search string + if 'T1' in filename: + return ps_data.Modality('T1') + elif 'T2' in filename: + return ps_data.Modality('T2') + else: + return None + + +def generate_distinct_colors(n): + """生成 n 個視覺上區分度高的顏色""" + colors = [] + for i in range(n): + # 使用黃金比例來產生分散的色相值 + hue = (i * 0.618033988749895) % 1 + # 使用較高的飽和度和亮度以確保顏色明顯 + saturation = 0.7 + (i % 3) * 0.1 # 0.7-0.9 + value = 0.8 + (i % 2) * 0.1 # 0.8-0.9 + + # 轉換 HSV 到 RGB + rgb = colorsys.hsv_to_rgb(hue, saturation, value) + # 轉換到 0-255 範圍 + color = [int(x * 255) for x in rgb] + colors.append(color) + + return colors + +# 設定顏色映射 +LABEL_COLORS: Dict[str, List[int]] = { + "Brainstem": [0, 255, 0], # 綠色 + "Right Eye": [255, 255, 224], # 淺黃色 + "Left Eye": [255, 255, 224], # 淺黃色 + "Optic Chiasm": [0, 0, 255], # 藍色 + "Right_Optic_Nerve": [255, 127, 80], # 珊瑚紅 + "Left_Optic_Nerve": [255, 127, 80] # 珊瑚紅 +} + +# 設定標籤映射 +LABEL_MAPPING: Dict[int, str] = { + 1: "Brainstem", + 2: "Right Eye", + 3: "Left Eye", + 4: "Optic Chiasm", + 5: "Right_Optic_Nerve", + 6: "Left_Optic_Nerve" +} + +# 設定日誌 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('medical_imaging.log') + ] +) +logger = logging.getLogger(__name__) + +def validate_dicom_directory(dicom_path): + """加強版 DICOM 目錄驗證""" + try: + if not os.path.exists(dicom_path): + return False, f"目錄不存在:{dicom_path}" + + if not os.listdir(dicom_path): + return False, f"目錄是空的:{dicom_path}" + + dicom_files = [] + non_dicom = [] + corrupt_files = [] + series_info = {} + + for filename in os.listdir(dicom_path): + filepath = os.path.join(dicom_path, filename) + if os.path.isfile(filepath): + try: + ds = dcmread(filepath) + if hasattr(ds, 'SeriesInstanceUID'): + series_uid = ds.SeriesInstanceUID + if series_uid not in series_info: + series_info[series_uid] = [] + series_info[series_uid].append(filepath) + dicom_files.append(filepath) + else: + non_dicom.append(filename) + except Exception as e: + logger.warning(f"無法讀取檔案 {filename}: {str(e)}") + if filename.endswith(('.dcm', '.DCM')): + corrupt_files.append(filename) + else: + non_dicom.append(filename) + + if not dicom_files: + message = f"在 {dicom_path} 中找不到有效的 DICOM 檔案\n" + if non_dicom: + message += f"發現 {len(non_dicom)} 個非 DICOM 檔案\n" + if corrupt_files: + message += f"發現 {len(corrupt_files)} 個損壞的 DICOM 檔案" + return False, message + + if len(series_info) > 1: + logger.warning(f"發現多個系列: {len(series_info)}") + + for series_uid, files in series_info.items(): + if len(files) < 5: + logger.warning(f"系列 {series_uid} 的影像數量過少: {len(files)}") + + return True, f"找到 {len(dicom_files)} 個有效的 DICOM 檔案,分屬 {len(series_info)} 個系列" + + except Exception as e: + return False, f"驗證過程發生錯誤:{str(e)}" + +def enhanced_dcm2nii(source_dir, output_dir): + """加強版的 dcm2nii 轉換函式""" + try: + is_valid, message = validate_dicom_directory(source_dir) + if not is_valid: + return False, message, [] + + os.makedirs(output_dir, exist_ok=True) + initial_files = set(os.listdir(output_dir)) + + converter = Dcm2niix() + converter.inputs.source_dir = source_dir + converter.inputs.output_dir = output_dir + converter.inputs.compress = 'y' + converter.inputs.merge_imgs = True + converter.inputs.single_file = True + converter.inputs.verbose = True + converter.terminal_output = 'stream' + + logger.info(f"執行 DICOM 轉換:{source_dir}") + try: + result = converter.run() + + if hasattr(result, 'runtime'): + if hasattr(result.runtime, 'stdout'): + logger.info(f"dcm2niix 輸出:\n{result.runtime.stdout}") + if hasattr(result.runtime, 'stderr'): + logger.warning(f"dcm2niix 錯誤:\n{result.runtime.stderr}") + except Exception as run_error: + logger.error(f"dcm2niix 執行錯誤:{str(run_error)}") + + final_files = set(os.listdir(output_dir)) + new_files = final_files - initial_files + nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + + if not nifti_files: + logger.warning("使用備用轉換選項重試...") + converter.inputs.merge_imgs = False + converter.inputs.single_file = False + try: + result = converter.run() + except Exception as retry_error: + logger.error(f"備用轉換選項執行錯誤:{str(retry_error)}") + + final_files = set(os.listdir(output_dir)) + new_files = final_files - initial_files + nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + + if not nifti_files: + return False, "轉換完成但未產生 NIfTI 檔案", [] + + valid_nifti = [] + for nii_file in nifti_files: + try: + img = sitk.ReadImage(os.path.join(output_dir, nii_file)) + if img.GetSize()[2] > 1: + valid_nifti.append(nii_file) + else: + logger.warning(f"檔案 {nii_file} 不是有效的3D影像") + except Exception as e: + logger.warning(f"無法讀取 NIfTI 檔案 {nii_file}: {str(e)}") + + if not valid_nifti: + return False, "轉換產生的 NIfTI 檔案無效", [] + + return True, f"成功轉換 {len(valid_nifti)} 個有效的 NIfTI 檔案", valid_nifti + + except Exception as e: + logger.error(f"DICOM 轉換錯誤:{str(e)}", exc_info=True) + return False, f"轉換過程發生錯誤:{str(e)}", [] + +def process_symmetric_structures(image_array): + """處理對稱結構的後處理函數""" + logger.info("開始處理對稱結構") + result_img = image_array.copy() + + # 定義需要處理的結構及其標籤 + structures = [ + {'name': '眼睛', 'labels': (2, 3), 'kernel_radius': 2}, + {'name': '視神經', 'labels': (5, 6), 'kernel_radius': 2} + ] + + # 處理每個對稱結構 + for structure in structures: + logger.info(f"處理{structure['name']}結構") + # 合併左右標籤 + combined_mask = np.zeros_like(result_img, dtype=np.uint8) + for label in structure['labels']: + combined_mask |= (result_img == label) + + # 形態學閉運算 + combined_mask_sitk = sitk.GetImageFromArray(combined_mask) + closing_filter = sitk.BinaryMorphologicalClosingImageFilter() + closing_filter.SetKernelRadius(structure['kernel_radius']) + processed_mask = sitk.GetArrayFromImage(closing_filter.Execute(combined_mask_sitk)) + + # 連通區域標記 + labeled_components = sitk.GetArrayFromImage( + sitk.ConnectedComponent(sitk.GetImageFromArray(processed_mask)) + ) + + # 找出最大的兩個區域 + unique, counts = np.unique(labeled_components, return_counts=True) + if len(unique) >= 3: # 確保至少有兩個區域(除了背景) + region_sizes = [(i, count) for i, count in zip(unique[1:], counts[1:])] + sorted_regions = sorted(region_sizes, key=lambda x: x[1], reverse=True) + + # 獲取最大的兩個區域 + region1 = (labeled_components == sorted_regions[0][0]) + region2 = (labeled_components == sorted_regions[1][0]) + + # 根據質心的x座標決定左右 + center1 = np.mean(np.where(region1)[2]) + center2 = np.mean(np.where(region2)[2]) + + # 分配左右標籤 + if center1 < center2: + right, left = region1, region2 + else: + right, left = region2, region1 + + # 更新結果 + result_img[right] = structure['labels'][0] # 右側標籤 + result_img[left] = structure['labels'][1] # 左側標籤 + + logger.info(f"完成{structure['name']}的左右結構處理") + else: + logger.warning(f"無法找到足夠的{structure['name']}區域進行處理") + + return result_img + +def process_multiple_tumors(image_array): + """處理多個腫瘤的函數""" + logger.info("開始處理多個腫瘤") + result_img = image_array.copy() + + # 假設原始標記中 7 代表腫瘤 + tumor_mask = (result_img == 7) + if not tumor_mask.any(): + logger.warning("未找到腫瘤區域") + return result_img + + # 使用連通區域分析找出不同的腫瘤 + tumor_mask_sitk = sitk.GetImageFromArray(tumor_mask.astype(np.uint8)) + connected_filter = sitk.ConnectedComponentImageFilter() + connected_filter.FullyConnectedOn() + labeled_tumors = sitk.GetArrayFromImage(connected_filter.Execute(tumor_mask_sitk)) + + num_tumors = connected_filter.GetObjectCount() + logger.info(f"找到 {num_tumors} 個腫瘤") + + if num_tumors == 0: + return result_img + + # 為每個腫瘤生成唯一的標籤值,從最大現有標籤值開始 + max_existing_label = max(LABEL_MAPPING.keys()) + tumor_colors = generate_distinct_colors(num_tumors) + + # 更新標籤映射和顏色映射 + for i in range(num_tumors): + tumor_label = max_existing_label + i + 1 + tumor_name = f"TV_{i+1}" + LABEL_MAPPING[tumor_label] = tumor_name + LABEL_COLORS[tumor_name] = tumor_colors[i] + + # 更新結果圖像中的腫瘤標籤 + result_img[labeled_tumors == (i + 1)] = tumor_label + logger.info(f"標記腫瘤 {tumor_name} 使用標籤 {tumor_label} 和顏色 {tumor_colors[i]}") + + return result_img + +def create_structure_masks(image: sitk.Image) -> Dict[str, np.ndarray]: + """從分割結果創建每個結構的遮罩""" + array = sitk.GetArrayFromImage(image) + masks = {} + + for label_id, structure_name in LABEL_MAPPING.items(): + mask = (array == label_id) + if mask.any(): + masks[structure_name] = np.transpose(mask, (1, 2, 0)) + + return masks + +def inference(DCM_CT, DCM_MR): + """執行影像推論和後處理""" + logger.info(f"處理影像\nCT: {DCM_CT}\nMR: {DCM_MR}") + + try: + # 驗證# 驗證輸入目錄 + for path, desc in [(DCM_CT, "CT"), (DCM_MR, "MR")]: + is_valid, message = validate_dicom_directory(path) + if not is_valid: + raise ValueError(f"{desc} 影像驗證失敗:{message}") + + # 設定工作目錄 + ROOT_DIR = os.path.dirname(os.path.dirname(DCM_CT)) + NII_DIR = os.path.join(ROOT_DIR, 'nii') + INPUT_DIR = os.path.join(ROOT_DIR, 'input') + OUTPUT_DIR = os.path.join(ROOT_DIR, 'output') + + # 設定 RTSTRUCT 輸出路徑 + head, tail = os.path.split(DCM_CT) + rtss_file = os.path.join(head, tail+'-rtss.dcm') + + # 清理並建立工作目錄 + for dir_path in [NII_DIR, INPUT_DIR, OUTPUT_DIR]: + shutil.rmtree(dir_path, ignore_errors=True) + os.makedirs(dir_path, exist_ok=True) + + # 取得基本檔名 + nCT = os.path.basename(DCM_CT) + nMR = os.path.basename(DCM_MR) + + # DICOM 轉 NIfTI + logger.info("開始轉換 CT DICOM...") + success_ct, message_ct, ct_files = enhanced_dcm2nii(DCM_CT, NII_DIR) + if not success_ct: + raise RuntimeError(f"CT 轉換失敗:{message_ct}") + + logger.info("開始轉換 MR DICOM...") + success_mr, message_mr, mr_files = enhanced_dcm2nii(DCM_MR, NII_DIR) + if not success_mr: + raise RuntimeError(f"MR 轉換失敗:{message_mr}") + + # 尋找轉換後的檔案 + NII_CT = None + NII_MR = None + for f in os.listdir(NII_DIR): + if f.endswith('.nii.gz'): + full_path = os.path.join(NII_DIR, f) + if f.startswith(nCT+'_'): + NII_CT = full_path + logger.info(f"找到 CT NIfTI 檔案:{f}") + elif f.startswith(nMR+'_'): + NII_MR = full_path + logger.info(f"找到 MR NIfTI 檔案:{f}") + + if not NII_CT: + raise FileNotFoundError(f"找不到 CT 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + if not NII_MR: + raise FileNotFoundError(f"找不到 MR 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + + # 準備輸入檔案 + basename = os.path.basename(NII_MR) + old = '_'+basename.split('_')[-1] + input_file = os.path.join(INPUT_DIR, basename.replace(old, '_0000.nii.gz')) + output_file = os.path.join(OUTPUT_DIR, basename.replace(old, '.nii.gz')) + + basename_ct = os.path.basename(NII_CT) + old_ct = '_'+basename_ct.split('_')[-1] + label_file = os.path.join(NII_DIR, basename_ct.replace(old_ct, '.label.nii.gz')) + + logger.info(f"複製 MR NIfTI 到輸入目錄:{input_file}") + shutil.copy(NII_MR, input_file) + + logger.info("準備執行模型推論...") + + # 設定 nnUNet 環境變數 + model_dir = "/123/onlylian" + os.environ['nnUNet_raw'] = "/123/onlylian/nnUNet_raw" + os.environ['nnUNet_preprocessed'] = "/123/onlylian/nnUNet_preprocessed" + os.environ['nnUNet_results'] = "/123/onlylian/nnUNet_results" + + # 明確設定可用的資料集路徑 + dataset_dir = os.path.join(model_dir, "nnUNet_results", "Dataset505") + logger.info(f"使用資料集目錄:{dataset_dir}") + + if not os.path.exists(dataset_dir): + logger.error(f"資料集目錄不存在:{dataset_dir}") + raise FileNotFoundError(f"找不到資料集目錄:{dataset_dir}") + + try: + # 執行模型推論 + predict_cmd = [ + "nnUNetv2_predict", + "-i", INPUT_DIR, + "-o", OUTPUT_DIR, + "-d", "505", + "-c", "3d_fullres", + "-f", "0", + "-tr", "nnUNetTrainer", + "-p", "nnUNetPlans" + ] + + logger.info(f"執行推論指令:{' '.join(predict_cmd)}") + result = subprocess.run( + predict_cmd, + check=True, + capture_output=True, + text=True, + env=os.environ + ) + + logger.info(f"模型推論輸出:\n{result.stdout}") + + except subprocess.CalledProcessError as e: + logger.error(f"模型推論失敗:\n輸出:{e.output}\n錯誤:{e.stderr}") + raise RuntimeError(f"模型推論失敗:{e.stderr}") + + if not os.path.exists(output_file): + raise FileNotFoundError(f"找不到模型輸出檔案:{output_file}") + + logger.info("開始執行影像後處理...") + + try: + # 讀取預測結果 + predicted_image = sitk.ReadImage(output_file) + predicted_image = sitk.DICOMOrient(predicted_image) + predicted_array = sitk.GetArrayFromImage(predicted_image) + + # 執行後處理 + processed_array = process_symmetric_structures(predicted_array) + processed_array = process_multiple_tumors(processed_array) + + processed_image = sitk.GetImageFromArray(processed_array) + processed_image.CopyInformation(predicted_image) + + # 暫時保存後處理結果 + processed_output = os.path.join(OUTPUT_DIR, 'processed_' + os.path.basename(output_file)) + sitk.WriteImage(processed_image, processed_output) + logger.info(f"後處理結果已保存至:{processed_output}") + + logger.info("開始執行影像配準...") + reg_transform(NII_CT, NII_MR, processed_output, label_file) + + except Exception as e: + logger.error(f"影像處理失敗:{str(e)}") + raise RuntimeError("後處理或配準失敗") + + if not os.path.exists(label_file): + raise FileNotFoundError(f"找不到配準後的標籤檔案:{label_file}") + + logger.info("開始建立 RTSTRUCT...") + + # 讀取 CT 影像序列 + try: + reader = sitk.ImageSeriesReader() + dicom_names = reader.GetGDCMSeriesFileNames(DCM_CT) + reader.SetFileNames(dicom_names) + reader.MetaDataDictionaryArrayUpdateOn() + reader.LoadPrivateTagsOn() + image = reader.Execute() + except Exception as e: + logger.error(f"讀取 CT 系列失敗:{str(e)}") + raise RuntimeError("無法讀取 CT 影像系列") + + # 讀取並重新取樣配準後的預測結果 + try: + final_image = sitk.ReadImage(label_file) + final_image = sitk.Resample( + final_image, + image, + sitk.Transform(), + sitk.sitkNearestNeighbor + ) + except Exception as e: + logger.error(f"重新取樣失敗:{str(e)}") + raise RuntimeError("預測結果重新取樣失敗") + + # 建立 RTSTRUCT + try: + + images = [] + images.append(IntensityImage(image, Modality('CT'))) + + for k, v in LABEL_MAPPING.items(): + images.append(SegmentationImage(final_image==k, v)) + + dcm_crawler = ps_io.SubjectDicomCrawler(DCM_CT, + modality_extractor=ExampleModalityExtractor(), + ) + dicom_series_info = dcm_crawler.execute() + + subject = Subject(dicom_series_info[0].get_patient_id(), images) + + reference_modality = 'CT' + + use_3d_conversion = False + if use_3d_conversion: + conv_conf = ps_io.RTSSConverter3DConfiguration() + else: + conv_conf = ps_io.RTSSConverter2DConfiguration() + meta_data = ps_io.RTSSMetaData( + series_description = '%s by onlylian'%type(conv_conf).__name__) + + converter = ps_io.SubjectToRTSSConverter( + subject, + dicom_series_info, + reference_modality, + conv_conf, + meta_data, +# colors = tuple(LABEL_COLORS.values()), + ) + + rtss = converter.convert() + + + # Write the DICOM-RTSS to a separate subject directory + # and include the DICOM files crawled before + # Note: If you want to output just a subset of the + # original DICOM files you may use additional selectors + + output_dir_path = os.path.join(ROOT_DIR, 'pyradise') + rtss_filename = os.path.basename(rtss_file) + rtss_combination = ((rtss_filename, rtss),) + print(output_dir_path) + print(rtss_combination) + +# shutil.rmtree(output_dir_path, ignore_errors=True) + os.makedirs(output_dir_path, exist_ok=True) + + writer = ps_io.DicomSeriesSubjectWriter() + writer.write(rtss_combination, + output_dir_path, +# subject.get_name(), + None, + dicom_series_info,) + + shutil.copy(os.path.join(output_dir_path, rtss_filename), rtss_file) + +# rtstruct = RTStructBuilder.create_new(dicom_series_path=DCM_CT) + +# # 為每個解剖結構創建遮罩 +# structure_masks = create_structure_masks(final_image) + +# # 確認是否有找到任何結構 +# if not structure_masks: +# logger.warning("未找到任何有效的結構遮罩") + +# # 添加每個結構到 RTSTRUCT +# for structure_name, mask in structure_masks.items(): +# if mask.any(): # 確保遮罩不是空的 +# logger.info(f"添加結構 {structure_name}") +# try: +# rtstruct.add_roi( +# mask=mask, +# color=LABEL_COLORS[structure_name], +# name=structure_name +# ) +# logger.info(f"成功添加 {structure_name}") +# except Exception as roi_error: +# logger.error(f"添加 ROI {structure_name} 時發生錯誤: {str(roi_error)}") +# continue + +# logger.info(f"儲存 RTSTRUCT:{rtss_file}") +# rtstruct.save(rtss_file) + + except Exception as e: + logger.error(f"RTSTRUCT 生成失敗:{str(e)}") + raise RuntimeError("無法生成或儲存 RTSTRUCT") + + return rtss_file + + except Exception as e: + logger.error(f"推論過程發生錯誤:{str(e)}", exc_info=True) + raise + +def SendDCM(fp): + """傳送 DICOM 檔案到 PACS""" + logger.info(f"準備傳送 DICOM 檔案:{fp}") + debug_logger() + + if not os.path.exists(fp): + raise FileNotFoundError(f"找不到 DICOM 檔案:{fp}") + + try: + ae = AE() + ae.ae_title = 'OUR_STORE_SCP' + ae.add_requested_context(RTStructureSetStorage) + + try: + ds = dcmread(fp) + except Exception as e: + logger.error(f"讀取 DICOM 檔案失敗:{str(e)}") + raise RuntimeError("無法讀取 DICOM 檔案") + + logger.info("正在連接 PACS 伺服器...") + try: + assoc = ae.associate("172.16.40.36", 104, ae_title='N1000_STORAGE') + except Exception as e: + logger.error(f"PACS 連接失敗:{str(e)}") + raise RuntimeError("無法連接 PACS 伺服器") + + if assoc.is_established: + try: + status = assoc.send_c_store(ds) + if status: + logger.info(f'DICOM 傳送成功,狀態碼: 0x{status.Status:04x}') + else: + raise RuntimeError('傳送失敗:連接逾時或收到無效回應') + except Exception as e: + logger.error(f"DICOM 傳送失敗:{str(e)}") + raise + finally: + assoc.release() + else: + raise RuntimeError('傳送失敗:無法建立連接') + + except Exception as e: + logger.error(f"DICOM 傳送過程發生錯誤:{str(e)}", exc_info=True) + raise + +def main(): + """主程式進入點""" + if len(sys.argv) < 3: + print('使用方式:', sys.argv[0], ' ') + print('範例: python oar.py /nn/3894670/20241111/CT/a /nn/3894670/20241111/MR/6') + sys.exit(1) + + logger.info('開始執行') + logger.info('程式名稱: %s', sys.argv[0]) + logger.info('CT路徑: %s', sys.argv[1]) + logger.info('MR路徑: %s', sys.argv[2]) + + start_time = time.time() + try: + rtss_file = inference(sys.argv[1], sys.argv[2]) + SendDCM(rtss_file) + logger.info(f"處理完成,總耗時:{time.time() - start_time:.2f} 秒") + except Exception as e: + logger.error(f"處理過程發生錯誤:{str(e)}", exc_info=True) + sys.exit(1) + +if __name__ == '__main__': + main() + \ No newline at end of file diff --git a/onlylian/oar3.py b/onlylian/oar3.py new file mode 100755 index 0000000..6551437 --- /dev/null +++ b/onlylian/oar3.py @@ -0,0 +1,729 @@ + +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import os +import shutil +import subprocess +import sys +import time +import logging +import colorsys + +from nipype.interfaces.dcm2nii import Dcm2niix +from pydicom import dcmread +from pynetdicom import AE, debug_logger +from pynetdicom.sop_class import CTImageStorage, RTStructureSetStorage +from rt_utils import RTStructBuilder + +import numpy as np +import SimpleITK as sitk + +from pyradise.data import (Subject, IntensityImage, SegmentationImage, + Modality, Organ, Annotator) +from pyradise.fileio.extraction import SimpleModalityExtractor +import pyradise.data as ps_data +import pyradise.fileio as ps_io + +from registration.best_reg import reg_only, reg_transform + +class ExampleModalityExtractor(ps_io.ModalityExtractor): + + def extract_from_dicom(self, + path: str + ) -> Optional[ps_data.Modality]: + # Extract the necessary attributes from the DICOM file + tags = (ps_io.Tag((0x0008, 0x0060)), # Modality + ps_io.Tag((0x0008, 0x103e))) # Series Description + dataset_dict = self._load_dicom_attributes(tags, path) + + # Identify the modality rule-based + modality = dataset_dict.get('Modality', {}).get('value', None) + series_desc = dataset_dict.get('Series Description', {}).get('value', '') + if modality == 'MR': + if 't1' in series_desc.lower(): + return ps_data.Modality('T1') + elif 't2' in series_desc.lower(): + return ps_data.Modality('T2') + else: + return None + elif modality == 'CT': + return ps_data.Modality('CT') + else: + return None + + def extract_from_path(self, + path: str + ) -> Optional[ps_data.Modality]: + + if 'CT' in path: + return ps_data.Modality('CT') + + # Identify the discrete image file's modality rule-based + filename = os.path.basename(path) + + # Check if the image contains an img prefix + # (i.e., it is a intensity image) + if not filename.startswith('img'): + return None + + # Check if the image contains a modality search string + if 'T1' in filename: + return ps_data.Modality('T1') + elif 'T2' in filename: + return ps_data.Modality('T2') + else: + return None + + +def generate_distinct_colors(n): + """生成 n 個視覺上區分度高的顏色""" + colors = [] + for i in range(n): + # 使用黃金比例來產生分散的色相值 + hue = (i * 0.618033988749895) % 1 + # 使用較高的飽和度和亮度以確保顏色明顯 + saturation = 0.7 + (i % 3) * 0.1 # 0.7-0.9 + value = 0.8 + (i % 2) * 0.1 # 0.8-0.9 + + # 轉換 HSV 到 RGB + rgb = colorsys.hsv_to_rgb(hue, saturation, value) + # 轉換到 0-255 範圍 + color = [int(x * 255) for x in rgb] + colors.append(color) + + return colors + +# 設定顏色映射 +LABEL_COLORS: Dict[str, List[int]] = { + "Brainstem": [0, 255, 0], # 綠色 + "Right Eye": [255, 255, 224], # 淺黃色 + "Left Eye": [255, 255, 224], # 淺黃色 + "Optic Chiasm": [0, 0, 255], # 藍色 + "Right_Optic_Nerve": [255, 127, 80], # 珊瑚紅 + "Left_Optic_Nerve": [255, 127, 80] # 珊瑚紅 +} + +# 設定標籤映射 +LABEL_MAPPING: Dict[int, str] = { + 1: "Brainstem", + 2: "Right Eye", + 3: "Left Eye", + 4: "Optic Chiasm", + 5: "Right_Optic_Nerve", + 6: "Left_Optic_Nerve" +} + +# 設定日誌 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('medical_imaging.log') + ] +) +logger = logging.getLogger(__name__) + +def validate_dicom_directory(dicom_path): + """加強版 DICOM 目錄驗證""" + try: + if not os.path.exists(dicom_path): + return False, f"目錄不存在:{dicom_path}" + + if not os.listdir(dicom_path): + return False, f"目錄是空的:{dicom_path}" + + dicom_files = [] + non_dicom = [] + corrupt_files = [] + series_info = {} + + for filename in os.listdir(dicom_path): + filepath = os.path.join(dicom_path, filename) + if os.path.isfile(filepath): + try: + ds = dcmread(filepath) + if hasattr(ds, 'SeriesInstanceUID'): + series_uid = ds.SeriesInstanceUID + if series_uid not in series_info: + series_info[series_uid] = [] + series_info[series_uid].append(filepath) + dicom_files.append(filepath) + else: + non_dicom.append(filename) + except Exception as e: + logger.warning(f"無法讀取檔案 {filename}: {str(e)}") + if filename.endswith(('.dcm', '.DCM')): + corrupt_files.append(filename) + else: + non_dicom.append(filename) + + if not dicom_files: + message = f"在 {dicom_path} 中找不到有效的 DICOM 檔案\n" + if non_dicom: + message += f"發現 {len(non_dicom)} 個非 DICOM 檔案\n" + if corrupt_files: + message += f"發現 {len(corrupt_files)} 個損壞的 DICOM 檔案" + return False, message + + if len(series_info) > 1: + logger.warning(f"發現多個系列: {len(series_info)}") + + for series_uid, files in series_info.items(): + if len(files) < 5: + logger.warning(f"系列 {series_uid} 的影像數量過少: {len(files)}") + + return True, f"找到 {len(dicom_files)} 個有效的 DICOM 檔案,分屬 {len(series_info)} 個系列" + + except Exception as e: + return False, f"驗證過程發生錯誤:{str(e)}" + +def enhanced_dcm2nii(source_dir, output_dir): + """加強版的 dcm2nii 轉換函式""" + try: + is_valid, message = validate_dicom_directory(source_dir) + if not is_valid: + return False, message, [] + + os.makedirs(output_dir, exist_ok=True) + initial_files = set(os.listdir(output_dir)) + + converter = Dcm2niix() + converter.inputs.source_dir = source_dir + converter.inputs.output_dir = output_dir + converter.inputs.compress = 'y' + converter.inputs.merge_imgs = True + converter.inputs.single_file = True + converter.inputs.verbose = True + converter.terminal_output = 'stream' + + logger.info(f"執行 DICOM 轉換:{source_dir}") + try: + result = converter.run() + + if hasattr(result, 'runtime'): + if hasattr(result.runtime, 'stdout'): + logger.info(f"dcm2niix 輸出:\n{result.runtime.stdout}") + if hasattr(result.runtime, 'stderr'): + logger.warning(f"dcm2niix 錯誤:\n{result.runtime.stderr}") + except Exception as run_error: + logger.error(f"dcm2niix 執行錯誤:{str(run_error)}") + + final_files = set(os.listdir(output_dir)) + new_files = final_files - initial_files + nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + + if not nifti_files: + logger.warning("使用備用轉換選項重試...") + converter.inputs.merge_imgs = False + converter.inputs.single_file = False + try: + result = converter.run() + except Exception as retry_error: + logger.error(f"備用轉換選項執行錯誤:{str(retry_error)}") + + final_files = set(os.listdir(output_dir)) + new_files = final_files - initial_files + nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + + if not nifti_files: + return False, "轉換完成但未產生 NIfTI 檔案", [] + + valid_nifti = [] + for nii_file in nifti_files: + try: + img = sitk.ReadImage(os.path.join(output_dir, nii_file)) + if img.GetSize()[2] > 1: + valid_nifti.append(nii_file) + else: + logger.warning(f"檔案 {nii_file} 不是有效的3D影像") + except Exception as e: + logger.warning(f"無法讀取 NIfTI 檔案 {nii_file}: {str(e)}") + + if not valid_nifti: + return False, "轉換產生的 NIfTI 檔案無效", [] + + return True, f"成功轉換 {len(valid_nifti)} 個有效的 NIfTI 檔案", valid_nifti + + except Exception as e: + logger.error(f"DICOM 轉換錯誤:{str(e)}", exc_info=True) + return False, f"轉換過程發生錯誤:{str(e)}", [] + +def process_symmetric_structures(image_array): + """處理對稱結構的後處理函數""" + logger.info("開始處理對稱結構") + result_img = image_array.copy() + + # 定義需要處理的結構及其標籤 + structures = [ + {'name': '眼睛', 'labels': (2, 3), 'kernel_radius': 2}, + {'name': '視神經', 'labels': (5, 6), 'kernel_radius': 2} + ] + + # 處理每個對稱結構 + for structure in structures: + logger.info(f"處理{structure['name']}結構") + # 合併左右標籤 + combined_mask = np.zeros_like(result_img, dtype=np.uint8) + for label in structure['labels']: + combined_mask |= (result_img == label) + + # 形態學閉運算 + combined_mask_sitk = sitk.GetImageFromArray(combined_mask) + closing_filter = sitk.BinaryMorphologicalClosingImageFilter() + closing_filter.SetKernelRadius(structure['kernel_radius']) + processed_mask = sitk.GetArrayFromImage(closing_filter.Execute(combined_mask_sitk)) + + # 連通區域標記 + labeled_components = sitk.GetArrayFromImage( + sitk.ConnectedComponent(sitk.GetImageFromArray(processed_mask)) + ) + + # 找出最大的兩個區域 + unique, counts = np.unique(labeled_components, return_counts=True) + if len(unique) >= 3: # 確保至少有兩個區域(除了背景) + region_sizes = [(i, count) for i, count in zip(unique[1:], counts[1:])] + sorted_regions = sorted(region_sizes, key=lambda x: x[1], reverse=True) + + # 獲取最大的兩個區域 + region1 = (labeled_components == sorted_regions[0][0]) + region2 = (labeled_components == sorted_regions[1][0]) + + # 根據質心的x座標決定左右 + center1 = np.mean(np.where(region1)[2]) + center2 = np.mean(np.where(region2)[2]) + + # 分配左右標籤 + if center1 < center2: + right, left = region1, region2 + else: + right, left = region2, region1 + + # 更新結果 + result_img[right] = structure['labels'][0] # 右側標籤 + result_img[left] = structure['labels'][1] # 左側標籤 + + logger.info(f"完成{structure['name']}的左右結構處理") + else: + logger.warning(f"無法找到足夠的{structure['name']}區域進行處理") + + return result_img + +def process_multiple_tumors(image_array): + """處理多個腫瘤的函數""" + logger.info("開始處理多個腫瘤") + result_img = image_array.copy() + + # 假設原始標記中 7 代表腫瘤 + tumor_mask = (result_img == 7) + if not tumor_mask.any(): + logger.warning("未找到腫瘤區域") + return result_img + + # 使用連通區域分析找出不同的腫瘤 + tumor_mask_sitk = sitk.GetImageFromArray(tumor_mask.astype(np.uint8)) + connected_filter = sitk.ConnectedComponentImageFilter() + connected_filter.FullyConnectedOn() + labeled_tumors = sitk.GetArrayFromImage(connected_filter.Execute(tumor_mask_sitk)) + + num_tumors = connected_filter.GetObjectCount() + logger.info(f"找到 {num_tumors} 個腫瘤") + + if num_tumors == 0: + return result_img + + # 為每個腫瘤生成唯一的標籤值,從最大現有標籤值開始 + max_existing_label = max(LABEL_MAPPING.keys()) + tumor_colors = generate_distinct_colors(num_tumors) + + # 更新標籤映射和顏色映射 + for i in range(num_tumors): + tumor_label = max_existing_label + i + 1 + tumor_name = f"TV_{i+1}" + LABEL_MAPPING[tumor_label] = tumor_name + LABEL_COLORS[tumor_name] = tumor_colors[i] + + # 更新結果圖像中的腫瘤標籤 + result_img[labeled_tumors == (i + 1)] = tumor_label + logger.info(f"標記腫瘤 {tumor_name} 使用標籤 {tumor_label} 和顏色 {tumor_colors[i]}") + + return result_img + +def create_structure_masks(image: sitk.Image) -> Dict[str, np.ndarray]: + """從分割結果創建每個結構的遮罩""" + array = sitk.GetArrayFromImage(image) + masks = {} + + for label_id, structure_name in LABEL_MAPPING.items(): + mask = (array == label_id) + if mask.any(): + masks[structure_name] = np.transpose(mask, (1, 2, 0)) + + return masks + +def inference(DCM_CT, DCM_MR): + """執行影像推論和後處理""" + logger.info(f"處理影像\nCT: {DCM_CT}\nMR: {DCM_MR}") + + try: + # 驗證# 驗證輸入目錄 + for path, desc in [(DCM_CT, "CT"), (DCM_MR, "MR")]: + is_valid, message = validate_dicom_directory(path) + if not is_valid: + raise ValueError(f"{desc} 影像驗證失敗:{message}") + + # 設定工作目錄 + ROOT_DIR = os.path.dirname(os.path.dirname(DCM_CT)) + NII_DIR = os.path.join(ROOT_DIR, 'nii') + INPUT_DIR = os.path.join(ROOT_DIR, 'input') + OUTPUT_DIR = os.path.join(ROOT_DIR, 'output') + + # 設定 RTSTRUCT 輸出路徑 + head, tail = os.path.split(DCM_CT) + rtss_file = os.path.join(head, tail+'-rtss.dcm') + + # 清理並建立工作目錄 + for dir_path in [NII_DIR, INPUT_DIR, OUTPUT_DIR]: + shutil.rmtree(dir_path, ignore_errors=True) + os.makedirs(dir_path, exist_ok=True) + + # 取得基本檔名 + nCT = os.path.basename(DCM_CT) + nMR = os.path.basename(DCM_MR) + + # DICOM 轉 NIfTI + logger.info("開始轉換 CT DICOM...") + success_ct, message_ct, ct_files = enhanced_dcm2nii(DCM_CT, NII_DIR) + if not success_ct: + raise RuntimeError(f"CT 轉換失敗:{message_ct}") + + logger.info("開始轉換 MR DICOM...") + success_mr, message_mr, mr_files = enhanced_dcm2nii(DCM_MR, NII_DIR) + if not success_mr: + raise RuntimeError(f"MR 轉換失敗:{message_mr}") + + # 尋找轉換後的檔案 + NII_CT = None + NII_MR = None + for f in os.listdir(NII_DIR): + if f.endswith('.nii.gz'): + full_path = os.path.join(NII_DIR, f) + if f.startswith(nCT+'_'): + NII_CT = full_path + logger.info(f"找到 CT NIfTI 檔案:{f}") + elif f.startswith(nMR+'_'): + NII_MR = full_path + logger.info(f"找到 MR NIfTI 檔案:{f}") + + if not NII_CT: + raise FileNotFoundError(f"找不到 CT 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + if not NII_MR: + raise FileNotFoundError(f"找不到 MR 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + + # 準備輸入檔案 + + basename = os.path.basename(NII_MR) + + old = '_'+basename.split('_')[-1] + + input_ct = os.path.join(INPUT_DIR, basename.replace(old, '_0000.nii.gz')) + shutil.copy(NII_CT, input_ct) + + input_mr = os.path.join(INPUT_DIR, basename.replace(old, '_0001.nii.gz')) + logger.info("Registration of %s and %s..."%(NII_CT, NII_MR)) + reg_only(NII_CT, NII_MR, input_mr) + + output_file = os.path.join(OUTPUT_DIR, basename.replace(old, '.nii.gz')) + + basename_ct = os.path.basename(NII_CT) + old_ct = '_'+basename_ct.split('_')[-1] + label_file = os.path.join(NII_DIR, basename_ct.replace(old_ct, '.label.nii.gz')) + +# basename = os.path.basename(NII_MR) +# old = '_'+basename.split('_')[-1] +# input_file = os.path.join(INPUT_DIR, basename.replace(old, '_0000.nii.gz')) +# output_file = os.path.join(OUTPUT_DIR, basename.replace(old, '.nii.gz')) + +# basename_ct = os.path.basename(NII_CT) +# old_ct = '_'+basename_ct.split('_')[-1] +# label_file = os.path.join(NII_DIR, basename_ct.replace(old_ct, '.label.nii.gz')) + +# logger.info(f"複製 MR NIfTI 到輸入目錄:{input_file}") +# shutil.copy(NII_MR, input_file) + + logger.info("準備執行模型推論...") + + # 設定 nnUNet 環境變數 + model_dir = "/123/onlylian" + os.environ['nnUNet_raw'] = "/123/onlylian/nnUNet_raw" + os.environ['nnUNet_preprocessed'] = "/123/onlylian/nnUNet_preprocessed" + os.environ['nnUNet_results'] = "/123/onlylian/nnUNet_results" + + # 明確設定可用的資料集路徑 + dataset_dir = os.path.join(model_dir, "nnUNet_results", "Dataset505") + logger.info(f"使用資料集目錄:{dataset_dir}") + + if not os.path.exists(dataset_dir): + logger.error(f"資料集目錄不存在:{dataset_dir}") + raise FileNotFoundError(f"找不到資料集目錄:{dataset_dir}") + + try: + # 執行模型推論 + predict_cmd = [ + "nnUNetv2_predict", + "-i", INPUT_DIR, + "-o", OUTPUT_DIR, +# "-d", "505", + "-d", "222", +# "-c", "3d_fullres", + "-c", "3d_lowres", +# "-f", "0", + "-tr", "nnUNetTrainer", + "-p", "nnUNetPlans" + ] + + logger.info(f"執行推論指令:{' '.join(predict_cmd)}") + result = subprocess.run( + predict_cmd, + check=True, + capture_output=True, + text=True, + env=os.environ + ) + + logger.info(f"模型推論輸出:\n{result.stdout}") + + except subprocess.CalledProcessError as e: + logger.error(f"模型推論失敗:\n輸出:{e.output}\n錯誤:{e.stderr}") + raise RuntimeError(f"模型推論失敗:{e.stderr}") + + if not os.path.exists(output_file): + raise FileNotFoundError(f"找不到模型輸出檔案:{output_file}") + + logger.info("開始執行影像後處理...") + + try: + # 讀取預測結果 + predicted_image = sitk.ReadImage(output_file) + predicted_image = sitk.DICOMOrient(predicted_image) + predicted_array = sitk.GetArrayFromImage(predicted_image) + + # 執行後處理 + processed_array = process_symmetric_structures(predicted_array) + processed_array = process_multiple_tumors(processed_array) + + processed_image = sitk.GetImageFromArray(processed_array) + processed_image.CopyInformation(predicted_image) + + # 暫時保存後處理結果 + processed_output = os.path.join(OUTPUT_DIR, 'processed_' + os.path.basename(output_file)) + sitk.WriteImage(processed_image, processed_output) + logger.info(f"後處理結果已保存至:{processed_output}") + + logger.info("開始執行影像配準...") +# reg_transform(NII_CT, NII_MR, processed_output, label_file) + shutil.copy(processed_output, label_file) + + except Exception as e: + logger.error(f"影像處理失敗:{str(e)}") + raise RuntimeError("後處理或配準失敗") + + if not os.path.exists(label_file): + raise FileNotFoundError(f"找不到配準後的標籤檔案:{label_file}") + + logger.info("開始建立 RTSTRUCT...") + + # 讀取 CT 影像序列 + try: + reader = sitk.ImageSeriesReader() + dicom_names = reader.GetGDCMSeriesFileNames(DCM_CT) + reader.SetFileNames(dicom_names) + reader.MetaDataDictionaryArrayUpdateOn() + reader.LoadPrivateTagsOn() + image = reader.Execute() + except Exception as e: + logger.error(f"讀取 CT 系列失敗:{str(e)}") + raise RuntimeError("無法讀取 CT 影像系列") + + # 讀取並重新取樣配準後的預測結果 + try: + final_image = sitk.ReadImage(label_file) + final_image = sitk.Resample( + final_image, + image, + sitk.Transform(), + sitk.sitkNearestNeighbor + ) + except Exception as e: + logger.error(f"重新取樣失敗:{str(e)}") + raise RuntimeError("預測結果重新取樣失敗") + + # 建立 RTSTRUCT + try: + + images = [] + images.append(IntensityImage(image, Modality('CT'))) +# images.append(IntensityImage(sitk.ReadImage(input_mr), Modality('MR'))) + + for k, v in LABEL_MAPPING.items(): + images.append(SegmentationImage(final_image==k, v)) + + dcm_crawler = ps_io.SubjectDicomCrawler(DCM_CT, + modality_extractor=ExampleModalityExtractor(), + ) + dicom_series_info = dcm_crawler.execute() + + subject = Subject(dicom_series_info[0].get_patient_id(), images) + + reference_modality = 'CT' + + use_3d_conversion = True + if use_3d_conversion: + conv_conf = ps_io.RTSSConverter3DConfiguration() + else: + conv_conf = ps_io.RTSSConverter2DConfiguration() + meta_data = ps_io.RTSSMetaData( + series_description = '%s by onlylian'%type(conv_conf).__name__) + + converter = ps_io.SubjectToRTSSConverter( + subject, + dicom_series_info, + reference_modality, + conv_conf, + meta_data, +# colors = tuple(LABEL_COLORS.values()), + ) + + rtss = converter.convert() + + + # Write the DICOM-RTSS to a separate subject directory + # and include the DICOM files crawled before + # Note: If you want to output just a subset of the + # original DICOM files you may use additional selectors + + output_dir_path = os.path.join(ROOT_DIR, 'pyradise') + rtss_filename = os.path.basename(rtss_file) + rtss_combination = ((rtss_filename, rtss),) + print(output_dir_path) + print(rtss_combination) + +# shutil.rmtree(output_dir_path, ignore_errors=True) + os.makedirs(output_dir_path, exist_ok=True) + + writer = ps_io.DicomSeriesSubjectWriter() + writer.write(rtss_combination, + output_dir_path, +# subject.get_name(), + None, + dicom_series_info,) + + shutil.copy(os.path.join(output_dir_path, rtss_filename), rtss_file) + +# rtstruct = RTStructBuilder.create_new(dicom_series_path=DCM_CT) + +# # 為每個解剖結構創建遮罩 +# structure_masks = create_structure_masks(final_image) + +# # 確認是否有找到任何結構 +# if not structure_masks: +# logger.warning("未找到任何有效的結構遮罩") + +# # 添加每個結構到 RTSTRUCT +# for structure_name, mask in structure_masks.items(): +# if mask.any(): # 確保遮罩不是空的 +# logger.info(f"添加結構 {structure_name}") +# try: +# rtstruct.add_roi( +# mask=mask, +# color=LABEL_COLORS[structure_name], +# name=structure_name +# ) +# logger.info(f"成功添加 {structure_name}") +# except Exception as roi_error: +# logger.error(f"添加 ROI {structure_name} 時發生錯誤: {str(roi_error)}") +# continue + +# logger.info(f"儲存 RTSTRUCT:{rtss_file}") +# rtstruct.save(rtss_file) + + except Exception as e: + logger.error(f"RTSTRUCT 生成失敗:{str(e)}") + raise RuntimeError("無法生成或儲存 RTSTRUCT") + + return rtss_file + + except Exception as e: + logger.error(f"推論過程發生錯誤:{str(e)}", exc_info=True) + raise + +def SendDCM(fp): + """傳送 DICOM 檔案到 PACS""" + logger.info(f"準備傳送 DICOM 檔案:{fp}") + debug_logger() + + if not os.path.exists(fp): + raise FileNotFoundError(f"找不到 DICOM 檔案:{fp}") + + try: + ae = AE() + ae.ae_title = 'OUR_STORE_SCP' + ae.add_requested_context(RTStructureSetStorage) + + try: + ds = dcmread(fp) + except Exception as e: + logger.error(f"讀取 DICOM 檔案失敗:{str(e)}") + raise RuntimeError("無法讀取 DICOM 檔案") + + logger.info("正在連接 PACS 伺服器...") + try: + assoc = ae.associate("172.16.40.36", 104, ae_title='N1000_STORAGE') + except Exception as e: + logger.error(f"PACS 連接失敗:{str(e)}") + raise RuntimeError("無法連接 PACS 伺服器") + + if assoc.is_established: + try: + status = assoc.send_c_store(ds) + if status: + logger.info(f'DICOM 傳送成功,狀態碼: 0x{status.Status:04x}') + else: + raise RuntimeError('傳送失敗:連接逾時或收到無效回應') + except Exception as e: + logger.error(f"DICOM 傳送失敗:{str(e)}") + raise + finally: + assoc.release() + else: + raise RuntimeError('傳送失敗:無法建立連接') + + except Exception as e: + logger.error(f"DICOM 傳送過程發生錯誤:{str(e)}", exc_info=True) + raise + +def main(): + """主程式進入點""" + if len(sys.argv) < 3: + print('使用方式:', sys.argv[0], ' ') + print('範例: python oar.py /nn/3894670/20241111/CT/a /nn/3894670/20241111/MR/6') + sys.exit(1) + + logger.info('開始執行') + logger.info('程式名稱: %s', sys.argv[0]) + logger.info('CT路徑: %s', sys.argv[1]) + logger.info('MR路徑: %s', sys.argv[2]) + + start_time = time.time() + try: + rtss_file = inference(sys.argv[1], sys.argv[2]) + SendDCM(rtss_file) + logger.info(f"處理完成,總耗時:{time.time() - start_time:.2f} 秒") + except Exception as e: + logger.error(f"處理過程發生錯誤:{str(e)}", exc_info=True) + sys.exit(1) + +if __name__ == '__main__': + main() + \ No newline at end of file diff --git a/onlylian/oar4.py b/onlylian/oar4.py new file mode 100755 index 0000000..425c087 --- /dev/null +++ b/onlylian/oar4.py @@ -0,0 +1,829 @@ + +''' +Use synthmorph + + +time python3 oar4.py /nn/7295866/20250127/CT/a /nn/7295866/20250127/MR/7 + +''' + +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import argparse +import os +import shutil +import subprocess +import sys +import tempfile +import time +import logging +import colorsys + +from nipype.interfaces.dcm2nii import Dcm2niix +from pydicom import dcmread +from pynetdicom import AE, debug_logger +from pynetdicom.sop_class import CTImageStorage, RTStructureSetStorage +from rt_utils import RTStructBuilder + +import numpy as np +import SimpleITK as sitk + +from pyradise.data import (Subject, IntensityImage, SegmentationImage, + Modality, Organ, Annotator) +from pyradise.fileio.extraction import SimpleModalityExtractor +import pyradise.data as ps_data +import pyradise.fileio as ps_io + +# from registration.best_reg import reg_only, reg_transform +import surfa as sf +# from mri_synthmorph.synthmorph import registration + +class ExampleModalityExtractor(ps_io.ModalityExtractor): + + def extract_from_dicom(self, + path: str + ) -> Optional[ps_data.Modality]: + # Extract the necessary attributes from the DICOM file + tags = (ps_io.Tag((0x0008, 0x0060)), # Modality + ps_io.Tag((0x0008, 0x103e))) # Series Description + dataset_dict = self._load_dicom_attributes(tags, path) + + # Identify the modality rule-based + modality = dataset_dict.get('Modality', {}).get('value', None) + series_desc = dataset_dict.get('Series Description', {}).get('value', '') + if modality == 'MR': + if 't1' in series_desc.lower(): + return ps_data.Modality('T1') + elif 't2' in series_desc.lower(): + return ps_data.Modality('T2') + else: + return None + elif modality == 'CT': + return ps_data.Modality('CT') + else: + return None + + def extract_from_path(self, + path: str + ) -> Optional[ps_data.Modality]: + + if 'CT' in path: + return ps_data.Modality('CT') + + # Identify the discrete image file's modality rule-based + filename = os.path.basename(path) + + # Check if the image contains an img prefix + # (i.e., it is a intensity image) + if not filename.startswith('img'): + return None + + # Check if the image contains a modality search string + if 'T1' in filename: + return ps_data.Modality('T1') + elif 'T2' in filename: + return ps_data.Modality('T2') + else: + return None + + +def generate_distinct_colors(n): + """生成 n 個視覺上區分度高的顏色""" + colors = [] + for i in range(n): + # 使用黃金比例來產生分散的色相值 + hue = (i * 0.618033988749895) % 1 + # 使用較高的飽和度和亮度以確保顏色明顯 + saturation = 0.7 + (i % 3) * 0.1 # 0.7-0.9 + value = 0.8 + (i % 2) * 0.1 # 0.8-0.9 + + # 轉換 HSV 到 RGB + rgb = colorsys.hsv_to_rgb(hue, saturation, value) + # 轉換到 0-255 範圍 + color = [int(x * 255) for x in rgb] + colors.append(color) + + return colors + +# 設定顏色映射 +LABEL_COLORS: Dict[str, List[int]] = { + "Brainstem": [0, 255, 0], # 綠色 + "Right Eye": [255, 255, 224], # 淺黃色 + "Left Eye": [255, 255, 224], # 淺黃色 + "Optic Chiasm": [0, 0, 255], # 藍色 + "Right_Optic_Nerve": [255, 127, 80], # 珊瑚紅 + "Left_Optic_Nerve": [255, 127, 80] # 珊瑚紅 +} + +# 設定標籤映射 +LABEL_MAPPING: Dict[int, str] = { + 1: "Brainstem", + 2: "Right Eye", + 3: "Left Eye", + 4: "Optic Chiasm", + 5: "Right_Optic_Nerve", + 6: "Left_Optic_Nerve" +} + +# 設定日誌 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('medical_imaging.log') + ] +) +logger = logging.getLogger(__name__) + +def validate_dicom_directory(dicom_path): + """加強版 DICOM 目錄驗證""" + try: + if not os.path.exists(dicom_path): + return False, f"目錄不存在:{dicom_path}" + + if not os.listdir(dicom_path): + return False, f"目錄是空的:{dicom_path}" + + dicom_files = [] + non_dicom = [] + corrupt_files = [] + series_info = {} + + for filename in os.listdir(dicom_path): + filepath = os.path.join(dicom_path, filename) + if os.path.isfile(filepath): + try: + ds = dcmread(filepath) + if hasattr(ds, 'SeriesInstanceUID'): + series_uid = ds.SeriesInstanceUID + if series_uid not in series_info: + series_info[series_uid] = [] + series_info[series_uid].append(filepath) + dicom_files.append(filepath) + else: + non_dicom.append(filename) + except Exception as e: + logger.warning(f"無法讀取檔案 {filename}: {str(e)}") + if filename.endswith(('.dcm', '.DCM')): + corrupt_files.append(filename) + else: + non_dicom.append(filename) + + if not dicom_files: + message = f"在 {dicom_path} 中找不到有效的 DICOM 檔案\n" + if non_dicom: + message += f"發現 {len(non_dicom)} 個非 DICOM 檔案\n" + if corrupt_files: + message += f"發現 {len(corrupt_files)} 個損壞的 DICOM 檔案" + return False, message + + if len(series_info) > 1: + logger.warning(f"發現多個系列: {len(series_info)}") + + for series_uid, files in series_info.items(): + if len(files) < 5: + logger.warning(f"系列 {series_uid} 的影像數量過少: {len(files)}") + + return True, f"找到 {len(dicom_files)} 個有效的 DICOM 檔案,分屬 {len(series_info)} 個系列" + + except Exception as e: + return False, f"驗證過程發生錯誤:{str(e)}" + +def enhanced_dcm2nii(source_dir, output_dir): + """加強版的 dcm2nii 轉換函式""" + try: + is_valid, message = validate_dicom_directory(source_dir) + if not is_valid: + return False, message, [] + + os.makedirs(output_dir, exist_ok=True) + initial_files = set(os.listdir(output_dir)) + + converter = Dcm2niix() + converter.inputs.source_dir = source_dir + converter.inputs.output_dir = output_dir + converter.inputs.compress = 'y' + converter.inputs.merge_imgs = True + converter.inputs.single_file = True + converter.inputs.verbose = True + converter.terminal_output = 'stream' + + logger.info(f"執行 DICOM 轉換:{source_dir}") + try: + result = converter.run() + + if hasattr(result, 'runtime'): + if hasattr(result.runtime, 'stdout'): + logger.info(f"dcm2niix 輸出:\n{result.runtime.stdout}") + if hasattr(result.runtime, 'stderr'): + logger.warning(f"dcm2niix 錯誤:\n{result.runtime.stderr}") + except Exception as run_error: + logger.error(f"dcm2niix 執行錯誤:{str(run_error)}") + + final_files = set(os.listdir(output_dir)) + new_files = final_files - initial_files + nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + + if not nifti_files: + logger.warning("使用備用轉換選項重試...") + converter.inputs.merge_imgs = False + converter.inputs.single_file = False + try: + result = converter.run() + except Exception as retry_error: + logger.error(f"備用轉換選項執行錯誤:{str(retry_error)}") + + final_files = set(os.listdir(output_dir)) + new_files = final_files - initial_files + nifti_files = [f for f in new_files if f.endswith(('.nii.gz', '.nii'))] + + if not nifti_files: + return False, "轉換完成但未產生 NIfTI 檔案", [] + + valid_nifti = [] + for nii_file in nifti_files: + try: + img = sitk.ReadImage(os.path.join(output_dir, nii_file)) + if img.GetSize()[2] > 1: + valid_nifti.append(nii_file) + else: + logger.warning(f"檔案 {nii_file} 不是有效的3D影像") + except Exception as e: + logger.warning(f"無法讀取 NIfTI 檔案 {nii_file}: {str(e)}") + + if not valid_nifti: + return False, "轉換產生的 NIfTI 檔案無效", [] + + return True, f"成功轉換 {len(valid_nifti)} 個有效的 NIfTI 檔案", valid_nifti + + except Exception as e: + logger.error(f"DICOM 轉換錯誤:{str(e)}", exc_info=True) + return False, f"轉換過程發生錯誤:{str(e)}", [] + +def process_symmetric_structures(image_array): + """處理對稱結構的後處理函數""" + logger.info("開始處理對稱結構") + result_img = image_array.copy() + + # 定義需要處理的結構及其標籤 + structures = [ + {'name': '眼睛', 'labels': (2, 3), 'kernel_radius': 2}, + {'name': '視神經', 'labels': (5, 6), 'kernel_radius': 2} + ] + + # 處理每個對稱結構 + for structure in structures: + logger.info(f"處理{structure['name']}結構") + # 合併左右標籤 + combined_mask = np.zeros_like(result_img, dtype=np.uint8) + for label in structure['labels']: + combined_mask |= (result_img == label) + + # 形態學閉運算 + combined_mask_sitk = sitk.GetImageFromArray(combined_mask) + closing_filter = sitk.BinaryMorphologicalClosingImageFilter() + closing_filter.SetKernelRadius(structure['kernel_radius']) + processed_mask = sitk.GetArrayFromImage(closing_filter.Execute(combined_mask_sitk)) + + # 連通區域標記 + labeled_components = sitk.GetArrayFromImage( + sitk.ConnectedComponent(sitk.GetImageFromArray(processed_mask)) + ) + + # 找出最大的兩個區域 + unique, counts = np.unique(labeled_components, return_counts=True) + if len(unique) >= 3: # 確保至少有兩個區域(除了背景) + region_sizes = [(i, count) for i, count in zip(unique[1:], counts[1:])] + sorted_regions = sorted(region_sizes, key=lambda x: x[1], reverse=True) + + # 獲取最大的兩個區域 + region1 = (labeled_components == sorted_regions[0][0]) + region2 = (labeled_components == sorted_regions[1][0]) + + # 根據質心的x座標決定左右 + center1 = np.mean(np.where(region1)[2]) + center2 = np.mean(np.where(region2)[2]) + + # 分配左右標籤 + if center1 < center2: + right, left = region1, region2 + else: + right, left = region2, region1 + + # 更新結果 + result_img[right] = structure['labels'][0] # 右側標籤 + result_img[left] = structure['labels'][1] # 左側標籤 + + logger.info(f"完成{structure['name']}的左右結構處理") + else: + logger.warning(f"無法找到足夠的{structure['name']}區域進行處理") + + return result_img + +def process_multiple_tumors(image_array): + """處理多個腫瘤的函數""" + logger.info("開始處理多個腫瘤") + result_img = image_array.copy() + + # 假設原始標記中 7 代表腫瘤 + tumor_mask = (result_img == 7) + if not tumor_mask.any(): + logger.warning("未找到腫瘤區域") + return result_img + + # 使用連通區域分析找出不同的腫瘤 + tumor_mask_sitk = sitk.GetImageFromArray(tumor_mask.astype(np.uint8)) + connected_filter = sitk.ConnectedComponentImageFilter() + connected_filter.FullyConnectedOn() + labeled_tumors = sitk.GetArrayFromImage(connected_filter.Execute(tumor_mask_sitk)) + + num_tumors = connected_filter.GetObjectCount() + logger.info(f"找到 {num_tumors} 個腫瘤") + + if num_tumors == 0: + return result_img + + # 為每個腫瘤生成唯一的標籤值,從最大現有標籤值開始 + max_existing_label = max(LABEL_MAPPING.keys()) + tumor_colors = generate_distinct_colors(num_tumors) + + # 更新標籤映射和顏色映射 + for i in range(num_tumors): + tumor_label = max_existing_label + i + 1 + tumor_name = f"TV_{i+1}" + LABEL_MAPPING[tumor_label] = tumor_name + LABEL_COLORS[tumor_name] = tumor_colors[i] + + # 更新結果圖像中的腫瘤標籤 + result_img[labeled_tumors == (i + 1)] = tumor_label + logger.info(f"標記腫瘤 {tumor_name} 使用標籤 {tumor_label} 和顏色 {tumor_colors[i]}") + + return result_img + +def create_structure_masks(image: sitk.Image) -> Dict[str, np.ndarray]: + """從分割結果創建每個結構的遮罩""" + array = sitk.GetArrayFromImage(image) + masks = {} + + for label_id, structure_name in LABEL_MAPPING.items(): + mask = (array == label_id) + if mask.any(): + masks[structure_name] = np.transpose(mask, (1, 2, 0)) + + return masks + +def register(ct_fixed, moving, out_moving): + + with tempfile.TemporaryDirectory() as tmp: + fixed = os.path.join(tmp, 'clipped.nii.gz') + + ct = sf.load_volume(ct_fixed) + clipped = ct.clip(0, 80) + clipped.save(fixed) + + FREESURFER_HOME = str(Path(__file__).resolve().parent/'mri_synthmorph') + + args = [ + str(Path(__file__).resolve().parent/'mri_synthmorph/mri_synthmorph'), +# 'register', + '-m', 'affine', +# '-m', 'rigid', + '-o', out_moving, + '-g', + moving, + fixed, + ] + + my_env = os.environ.copy() + my_env["FREESURFER_HOME"] = str(Path(__file__).resolve().parent/'mri_synthmorph') + + +# logger.info(f"執行推論指令:{' '.join(predict_cmd)}") + result = subprocess.run( + args, + env=my_env, + ) + +''' +Namespace(command='register', extent=256, fixed='0/clipped.nii.gz', gpu=True, header_only=False, hyper=0.5, init=None, inverse=None, mid_space=False, model='affine', moving='0/t1c.nii.gz', out_dir=None, out_fixed=None, out_moving='0/out-aff.nii.gz', steps=7, threads=None, trans='0/trans.lta', verbose=False, weights=None) +''' + +def register_synthmorph(ct_fixed, moving, out_moving): + + with tempfile.TemporaryDirectory() as tmp: + fixed = os.path.join(tmp, 'clipped.nii.gz') + + ct = sf.load_volume(ct_fixed) + clipped = ct.clip(0, 80) + clipped.save(fixed) + + default = { +# 'model': 'joint', + 'model': 'affine', + 'hyper': 0.5, + 'extent': 256, + 'steps': 7, + 'method': 'linear', + 'type': 'float32', + 'fill': 0, + + 'command': 'register', + 'gpu': True, + 'header_only': False, + 'init': None, + 'inverse': None, + 'mid_space': False, + + 'out_dir': None, + 'out_fixed': None, + + 'threads': None, + 'trans': None, +# 'verbose': False, + 'verbose': True, + 'weights': None, + + 'fixed' : fixed, + 'moving' : moving, + 'out_moving': out_moving, + +# 'weights': str(Path(__file__).resolve().parent/'mri_synthmorph/models/synthmorph.affine.2.h5'), + + } + + arg=argparse.Namespace(**default) + FREESURFER_HOME = str(Path(__file__).resolve().parent/'mri_synthmorph') + print(arg) + print(FREESURFER_HOME) + + os.environ["FREESURFER_HOME"] = str(Path(__file__).resolve().parent/'mri_synthmorph') + registration.register(arg) + +def inference(DCM_CT, DCM_MR): + """執行影像推論和後處理""" + logger.info(f"處理影像\nCT: {DCM_CT}\nMR: {DCM_MR}") + + try: + # 驗證# 驗證輸入目錄 + for path, desc in [(DCM_CT, "CT"), (DCM_MR, "MR")]: + is_valid, message = validate_dicom_directory(path) + if not is_valid: + raise ValueError(f"{desc} 影像驗證失敗:{message}") + + # 設定工作目錄 + ROOT_DIR = os.path.dirname(os.path.dirname(DCM_CT)) + NII_DIR = os.path.join(ROOT_DIR, 'nii') + INPUT_DIR = os.path.join(ROOT_DIR, 'input') + OUTPUT_DIR = os.path.join(ROOT_DIR, 'output') + + # 設定 RTSTRUCT 輸出路徑 + head, tail = os.path.split(DCM_CT) + rtss_file = os.path.join(head, tail+'-rtss.dcm') + + # 清理並建立工作目錄 + for dir_path in [NII_DIR, INPUT_DIR, OUTPUT_DIR]: + shutil.rmtree(dir_path, ignore_errors=True) + os.makedirs(dir_path, exist_ok=True) + + # 取得基本檔名 + nCT = os.path.basename(DCM_CT) + nMR = os.path.basename(DCM_MR) + + # DICOM 轉 NIfTI + logger.info("開始轉換 CT DICOM...") + success_ct, message_ct, ct_files = enhanced_dcm2nii(DCM_CT, NII_DIR) + if not success_ct: + raise RuntimeError(f"CT 轉換失敗:{message_ct}") + + logger.info("開始轉換 MR DICOM...") + success_mr, message_mr, mr_files = enhanced_dcm2nii(DCM_MR, NII_DIR) + if not success_mr: + raise RuntimeError(f"MR 轉換失敗:{message_mr}") + + # 尋找轉換後的檔案 + NII_CT = None + NII_MR = None + for f in os.listdir(NII_DIR): + if f.endswith('.nii.gz'): + full_path = os.path.join(NII_DIR, f) + if f.startswith(nCT+'_'): + NII_CT = full_path + logger.info(f"找到 CT NIfTI 檔案:{f}") + elif f.startswith(nMR+'_'): + NII_MR = full_path + logger.info(f"找到 MR NIfTI 檔案:{f}") + + if not NII_CT: + raise FileNotFoundError(f"找不到 CT 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + if not NII_MR: + raise FileNotFoundError(f"找不到 MR 的 NIfTI 檔案,目錄內容:{os.listdir(NII_DIR)}") + + # 準備輸入檔案 + + basename = os.path.basename(NII_MR) + + old = '_'+basename.split('_')[-1] + + input_ct = os.path.join(INPUT_DIR, basename.replace(old, '_0000.nii.gz')) + shutil.copy(NII_CT, input_ct) + + input_mr = os.path.join(INPUT_DIR, basename.replace(old, '_0001.nii.gz')) + + logger.info("Registration of %s and %s..."%(NII_CT, NII_MR)) + register(NII_CT, NII_MR, input_mr) + + output_file = os.path.join(OUTPUT_DIR, basename.replace(old, '.nii.gz')) + + basename_ct = os.path.basename(NII_CT) + old_ct = '_'+basename_ct.split('_')[-1] + label_file = os.path.join(NII_DIR, basename_ct.replace(old_ct, '.label.nii.gz')) + +# basename = os.path.basename(NII_MR) +# old = '_'+basename.split('_')[-1] +# input_file = os.path.join(INPUT_DIR, basename.replace(old, '_0000.nii.gz')) +# output_file = os.path.join(OUTPUT_DIR, basename.replace(old, '.nii.gz')) + +# basename_ct = os.path.basename(NII_CT) +# old_ct = '_'+basename_ct.split('_')[-1] +# label_file = os.path.join(NII_DIR, basename_ct.replace(old_ct, '.label.nii.gz')) + +# logger.info(f"複製 MR NIfTI 到輸入目錄:{input_file}") +# shutil.copy(NII_MR, input_file) + + logger.info("準備執行模型推論...") + + # 設定 nnUNet 環境變數 + model_dir = "/123/onlylian" + os.environ['nnUNet_raw'] = "/123/onlylian/nnUNet_raw" + os.environ['nnUNet_preprocessed'] = "/123/onlylian/nnUNet_preprocessed" + os.environ['nnUNet_results'] = "/123/onlylian/nnUNet_results" + + # 明確設定可用的資料集路徑 + dataset_dir = os.path.join(model_dir, "nnUNet_results", "Dataset505") + logger.info(f"使用資料集目錄:{dataset_dir}") + + if not os.path.exists(dataset_dir): + logger.error(f"資料集目錄不存在:{dataset_dir}") + raise FileNotFoundError(f"找不到資料集目錄:{dataset_dir}") + + try: + # 執行模型推論 + predict_cmd = [ + "nnUNetv2_predict", + "-i", INPUT_DIR, + "-o", OUTPUT_DIR, +# "-d", "505", + "-d", "222", +# "-c", "3d_fullres", + "-c", "3d_lowres", +# "-f", "0", + "-tr", "nnUNetTrainer", + "-p", "nnUNetPlans" + ] + + logger.info(f"執行推論指令:{' '.join(predict_cmd)}") + result = subprocess.run( + predict_cmd, + check=True, + capture_output=True, + text=True, + env=os.environ + ) + + logger.info(f"模型推論輸出:\n{result.stdout}") + + except subprocess.CalledProcessError as e: + logger.error(f"模型推論失敗:\n輸出:{e.output}\n錯誤:{e.stderr}") + raise RuntimeError(f"模型推論失敗:{e.stderr}") + + if not os.path.exists(output_file): + raise FileNotFoundError(f"找不到模型輸出檔案:{output_file}") + + logger.info("開始執行影像後處理...") + + try: + # 讀取預測結果 + predicted_image = sitk.ReadImage(output_file) + predicted_image = sitk.DICOMOrient(predicted_image) + predicted_array = sitk.GetArrayFromImage(predicted_image) + + # 執行後處理 + processed_array = process_symmetric_structures(predicted_array) + processed_array = process_multiple_tumors(processed_array) + + processed_image = sitk.GetImageFromArray(processed_array) + processed_image.CopyInformation(predicted_image) + + # 暫時保存後處理結果 + processed_output = os.path.join(OUTPUT_DIR, 'processed_' + os.path.basename(output_file)) + sitk.WriteImage(processed_image, processed_output) + logger.info(f"後處理結果已保存至:{processed_output}") + + logger.info("開始執行影像配準...") +# reg_transform(NII_CT, NII_MR, processed_output, label_file) + shutil.copy(processed_output, label_file) + + except Exception as e: + logger.error(f"影像處理失敗:{str(e)}") + raise RuntimeError("後處理或配準失敗") + + if not os.path.exists(label_file): + raise FileNotFoundError(f"找不到配準後的標籤檔案:{label_file}") + + logger.info("開始建立 RTSTRUCT...") + + # 讀取 CT 影像序列 + try: + reader = sitk.ImageSeriesReader() + dicom_names = reader.GetGDCMSeriesFileNames(DCM_CT) + reader.SetFileNames(dicom_names) + reader.MetaDataDictionaryArrayUpdateOn() + reader.LoadPrivateTagsOn() + image = reader.Execute() + except Exception as e: + logger.error(f"讀取 CT 系列失敗:{str(e)}") + raise RuntimeError("無法讀取 CT 影像系列") + + # 讀取並重新取樣配準後的預測結果 + try: + final_image = sitk.ReadImage(label_file) + final_image = sitk.Resample( + final_image, + image, + sitk.Transform(), + sitk.sitkNearestNeighbor + ) + except Exception as e: + logger.error(f"重新取樣失敗:{str(e)}") + raise RuntimeError("預測結果重新取樣失敗") + + # 建立 RTSTRUCT + try: + + images = [] + images.append(IntensityImage(image, Modality('CT'))) +# images.append(IntensityImage(sitk.ReadImage(input_mr), Modality('MR'))) + + for k, v in LABEL_MAPPING.items(): + images.append(SegmentationImage(final_image==k, v)) + + dcm_crawler = ps_io.SubjectDicomCrawler(DCM_CT, + modality_extractor=ExampleModalityExtractor(), + ) + dicom_series_info = dcm_crawler.execute() + + subject = Subject(dicom_series_info[0].get_patient_id(), images) + + reference_modality = 'CT' + + use_3d_conversion = True + if use_3d_conversion: + conv_conf = ps_io.RTSSConverter3DConfiguration() + else: + conv_conf = ps_io.RTSSConverter2DConfiguration() + meta_data = ps_io.RTSSMetaData( + series_description = '%s by onlylian'%type(conv_conf).__name__) + + converter = ps_io.SubjectToRTSSConverter( + subject, + dicom_series_info, + reference_modality, + conv_conf, + meta_data, +# colors = tuple(LABEL_COLORS.values()), + ) + + rtss = converter.convert() + + + # Write the DICOM-RTSS to a separate subject directory + # and include the DICOM files crawled before + # Note: If you want to output just a subset of the + # original DICOM files you may use additional selectors + + output_dir_path = os.path.join(ROOT_DIR, 'pyradise') + rtss_filename = os.path.basename(rtss_file) + rtss_combination = ((rtss_filename, rtss),) + print(output_dir_path) + print(rtss_combination) + +# shutil.rmtree(output_dir_path, ignore_errors=True) + os.makedirs(output_dir_path, exist_ok=True) + + writer = ps_io.DicomSeriesSubjectWriter() + writer.write(rtss_combination, + output_dir_path, +# subject.get_name(), + None, + dicom_series_info,) + + shutil.copy(os.path.join(output_dir_path, rtss_filename), rtss_file) + +# rtstruct = RTStructBuilder.create_new(dicom_series_path=DCM_CT) + +# # 為每個解剖結構創建遮罩 +# structure_masks = create_structure_masks(final_image) + +# # 確認是否有找到任何結構 +# if not structure_masks: +# logger.warning("未找到任何有效的結構遮罩") + +# # 添加每個結構到 RTSTRUCT +# for structure_name, mask in structure_masks.items(): +# if mask.any(): # 確保遮罩不是空的 +# logger.info(f"添加結構 {structure_name}") +# try: +# rtstruct.add_roi( +# mask=mask, +# color=LABEL_COLORS[structure_name], +# name=structure_name +# ) +# logger.info(f"成功添加 {structure_name}") +# except Exception as roi_error: +# logger.error(f"添加 ROI {structure_name} 時發生錯誤: {str(roi_error)}") +# continue + +# logger.info(f"儲存 RTSTRUCT:{rtss_file}") +# rtstruct.save(rtss_file) + + except Exception as e: + logger.error(f"RTSTRUCT 生成失敗:{str(e)}") + raise RuntimeError("無法生成或儲存 RTSTRUCT") + + return rtss_file + + except Exception as e: + logger.error(f"推論過程發生錯誤:{str(e)}", exc_info=True) + raise + +def SendDCM(fp): + """傳送 DICOM 檔案到 PACS""" + logger.info(f"準備傳送 DICOM 檔案:{fp}") + debug_logger() + + if not os.path.exists(fp): + raise FileNotFoundError(f"找不到 DICOM 檔案:{fp}") + + try: + ae = AE() + ae.ae_title = 'OUR_STORE_SCP' + ae.add_requested_context(RTStructureSetStorage) + + try: + ds = dcmread(fp) + except Exception as e: + logger.error(f"讀取 DICOM 檔案失敗:{str(e)}") + raise RuntimeError("無法讀取 DICOM 檔案") + + logger.info("正在連接 PACS 伺服器...") + try: + assoc = ae.associate("172.16.40.36", 104, ae_title='N1000_STORAGE') + except Exception as e: + logger.error(f"PACS 連接失敗:{str(e)}") + raise RuntimeError("無法連接 PACS 伺服器") + + if assoc.is_established: + try: + status = assoc.send_c_store(ds) + if status: + logger.info(f'DICOM 傳送成功,狀態碼: 0x{status.Status:04x}') + else: + raise RuntimeError('傳送失敗:連接逾時或收到無效回應') + except Exception as e: + logger.error(f"DICOM 傳送失敗:{str(e)}") + raise + finally: + assoc.release() + else: + raise RuntimeError('傳送失敗:無法建立連接') + + except Exception as e: + logger.error(f"DICOM 傳送過程發生錯誤:{str(e)}", exc_info=True) + raise + +def main(): + """主程式進入點""" + if len(sys.argv) < 3: + print('使用方式:', sys.argv[0], ' ') + print('範例: python oar.py /nn/3894670/20241111/CT/a /nn/3894670/20241111/MR/6') + sys.exit(1) + + logger.info('開始執行') + logger.info('程式名稱: %s', sys.argv[0]) + logger.info('CT路徑: %s', sys.argv[1]) + logger.info('MR路徑: %s', sys.argv[2]) + + start_time = time.time() + try: + rtss_file = inference(sys.argv[1], sys.argv[2]) + SendDCM(rtss_file) + logger.info(f"處理完成,總耗時:{time.time() - start_time:.2f} 秒") + except Exception as e: + logger.error(f"處理過程發生錯誤:{str(e)}", exc_info=True) + sys.exit(1) + +if __name__ == '__main__': + main() + \ No newline at end of file diff --git a/onlylian/onlylian/dataset.json b/onlylian/onlylian/dataset.json new file mode 100755 index 0000000..5c5901e --- /dev/null +++ b/onlylian/onlylian/dataset.json @@ -0,0 +1,17 @@ +{ + "channel_names": { + "0": "T1C" + }, + "labels": { + "background": "0", + "Brainstem": "1", + "Right_Eye": "2", + "Left_Eye": "3", + "Optic_Chiasm": "4", + "Right_Optic_Nerve": "5", + "Left_Optic_Nerve": "6", + "TV": "7" + }, + "numTraining": 3083, + "file_ending": ".nii.gz" +} \ No newline at end of file diff --git a/onlylian/onlylian/output/dataset.json b/onlylian/onlylian/output/dataset.json new file mode 100755 index 0000000..5c5901e --- /dev/null +++ b/onlylian/onlylian/output/dataset.json @@ -0,0 +1,17 @@ +{ + "channel_names": { + "0": "T1C" + }, + "labels": { + "background": "0", + "Brainstem": "1", + "Right_Eye": "2", + "Left_Eye": "3", + "Optic_Chiasm": "4", + "Right_Optic_Nerve": "5", + "Left_Optic_Nerve": "6", + "TV": "7" + }, + "numTraining": 3083, + "file_ending": ".nii.gz" +} \ No newline at end of file diff --git a/onlylian/onlylian/output/plans.json b/onlylian/onlylian/output/plans.json new file mode 100755 index 0000000..3394f22 --- /dev/null +++ b/onlylian/onlylian/output/plans.json @@ -0,0 +1,532 @@ +{ + "dataset_name": "Dataset012_OAR_TV", + "plans_name": "nnUNetPlans", + "original_median_spacing_after_transp": [ + 1.25, + 0.5859379768371582, + 0.5859379768371582 + ], + "original_median_shape_after_transp": [ + 159, + 497, + 505 + ], + "image_reader_writer": "SimpleITKIO", + "transpose_forward": [ + 0, + 1, + 2 + ], + "transpose_backward": [ + 0, + 1, + 2 + ], + "configurations": { + "2d": { + "data_identifier": "nnUNetPlans_2d", + "preprocessor_name": "DefaultPreprocessor", + "batch_size": 12, + "patch_size": [ + 512, + 512 + ], + "median_image_size_in_voxels": [ + 497.0, + 505.0 + ], + "spacing": [ + 0.5859379768371582, + 0.5859379768371582 + ], + "normalization_schemes": [ + "ZScoreNormalization" + ], + "use_mask_for_norm": [ + true + ], + "resampling_fn_data": "resample_data_or_seg_to_shape", + "resampling_fn_seg": "resample_data_or_seg_to_shape", + "resampling_fn_data_kwargs": { + "is_seg": false, + "order": 3, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_seg_kwargs": { + "is_seg": true, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_probabilities": "resample_data_or_seg_to_shape", + "resampling_fn_probabilities_kwargs": { + "is_seg": false, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "architecture": { + "network_class_name": "dynamic_network_architectures.architectures.unet.PlainConvUNet", + "arch_kwargs": { + "n_stages": 8, + "features_per_stage": [ + 32, + 64, + 128, + 256, + 512, + 512, + 512, + 512 + ], + "conv_op": "torch.nn.modules.conv.Conv2d", + "kernel_sizes": [ + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ] + ], + "strides": [ + [ + 1, + 1 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ] + ], + "n_conv_per_stage": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "n_conv_per_stage_decoder": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "conv_bias": true, + "norm_op": "torch.nn.modules.instancenorm.InstanceNorm2d", + "norm_op_kwargs": { + "eps": 1e-05, + "affine": true + }, + "dropout_op": null, + "dropout_op_kwargs": null, + "nonlin": "torch.nn.LeakyReLU", + "nonlin_kwargs": { + "inplace": true + } + }, + "_kw_requires_import": [ + "conv_op", + "norm_op", + "dropout_op", + "nonlin" + ] + }, + "batch_dice": true + }, + "3d_lowres": { + "data_identifier": "nnUNetPlans_3d_lowres", + "preprocessor_name": "DefaultPreprocessor", + "batch_size": 2, + "patch_size": [ + 64, + 192, + 192 + ], + "median_image_size_in_voxels": [ + 102, + 301, + 306 + ], + "spacing": [ + 1.89073715606889, + 0.9684661976733974, + 0.9684661976733974 + ], + "normalization_schemes": [ + "ZScoreNormalization" + ], + "use_mask_for_norm": [ + true + ], + "resampling_fn_data": "resample_data_or_seg_to_shape", + "resampling_fn_seg": "resample_data_or_seg_to_shape", + "resampling_fn_data_kwargs": { + "is_seg": false, + "order": 3, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_seg_kwargs": { + "is_seg": true, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_probabilities": "resample_data_or_seg_to_shape", + "resampling_fn_probabilities_kwargs": { + "is_seg": false, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "architecture": { + "network_class_name": "dynamic_network_architectures.architectures.unet.PlainConvUNet", + "arch_kwargs": { + "n_stages": 6, + "features_per_stage": [ + 32, + 64, + 128, + 256, + 320, + 320 + ], + "conv_op": "torch.nn.modules.conv.Conv3d", + "kernel_sizes": [ + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ] + ], + "strides": [ + [ + 1, + 1, + 1 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 1, + 2, + 2 + ] + ], + "n_conv_per_stage": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "n_conv_per_stage_decoder": [ + 2, + 2, + 2, + 2, + 2 + ], + "conv_bias": true, + "norm_op": "torch.nn.modules.instancenorm.InstanceNorm3d", + "norm_op_kwargs": { + "eps": 1e-05, + "affine": true + }, + "dropout_op": null, + "dropout_op_kwargs": null, + "nonlin": "torch.nn.LeakyReLU", + "nonlin_kwargs": { + "inplace": true + } + }, + "_kw_requires_import": [ + "conv_op", + "norm_op", + "dropout_op", + "nonlin" + ] + }, + "batch_dice": false, + "next_stage": "3d_cascade_fullres" + }, + "3d_fullres": { + "data_identifier": "nnUNetPlans_3d_fullres", + "preprocessor_name": "DefaultPreprocessor", + "batch_size": 2, + "patch_size": [ + 56, + 160, + 192 + ], + "median_image_size_in_voxels": [ + 155.0, + 497.0, + 505.0 + ], + "spacing": [ + 1.25, + 0.5859379768371582, + 0.5859379768371582 + ], + "normalization_schemes": [ + "ZScoreNormalization" + ], + "use_mask_for_norm": [ + true + ], + "resampling_fn_data": "resample_data_or_seg_to_shape", + "resampling_fn_seg": "resample_data_or_seg_to_shape", + "resampling_fn_data_kwargs": { + "is_seg": false, + "order": 3, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_seg_kwargs": { + "is_seg": true, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_probabilities": "resample_data_or_seg_to_shape", + "resampling_fn_probabilities_kwargs": { + "is_seg": false, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "architecture": { + "network_class_name": "dynamic_network_architectures.architectures.unet.PlainConvUNet", + "arch_kwargs": { + "n_stages": 6, + "features_per_stage": [ + 32, + 64, + 128, + 256, + 320, + 320 + ], + "conv_op": "torch.nn.modules.conv.Conv3d", + "kernel_sizes": [ + [ + 1, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ] + ], + "strides": [ + [ + 1, + 1, + 1 + ], + [ + 1, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 1, + 2, + 2 + ] + ], + "n_conv_per_stage": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "n_conv_per_stage_decoder": [ + 2, + 2, + 2, + 2, + 2 + ], + "conv_bias": true, + "norm_op": "torch.nn.modules.instancenorm.InstanceNorm3d", + "norm_op_kwargs": { + "eps": 1e-05, + "affine": true + }, + "dropout_op": null, + "dropout_op_kwargs": null, + "nonlin": "torch.nn.LeakyReLU", + "nonlin_kwargs": { + "inplace": true + } + }, + "_kw_requires_import": [ + "conv_op", + "norm_op", + "dropout_op", + "nonlin" + ] + }, + "batch_dice": true + }, + "3d_cascade_fullres": { + "inherits_from": "3d_fullres", + "previous_stage": "3d_lowres" + } + }, + "experiment_planner_used": "ExperimentPlanner", + "label_manager": "LabelManager", + "foreground_intensity_properties_per_channel": { + "0": { + "max": 13339.3759765625, + "mean": 534.5079307033803, + "median": 364.7799987792969, + "min": -3802.36328125, + "percentile_00_5": 0.0, + "percentile_99_5": 3067.902587890625, + "std": 528.3204154692955 + } + } +} \ No newline at end of file diff --git a/onlylian/onlylian/output/predict_from_raw_data_args.json b/onlylian/onlylian/output/predict_from_raw_data_args.json new file mode 100755 index 0000000..85b86de --- /dev/null +++ b/onlylian/onlylian/output/predict_from_raw_data_args.json @@ -0,0 +1,11 @@ +{ + "folder_with_segs_from_prev_stage": null, + "list_of_lists_or_source_folder": "/nn/7208298/20241213/input", + "num_parts": 1, + "num_processes_preprocessing": 3, + "num_processes_segmentation_export": 3, + "output_folder_or_list_of_truncated_output_files": "/nn/7208298/20241213/output", + "overwrite": true, + "part_id": 0, + "save_probabilities": false +} \ No newline at end of file diff --git a/onlylian/onlylian/plans.json b/onlylian/onlylian/plans.json new file mode 100755 index 0000000..3394f22 --- /dev/null +++ b/onlylian/onlylian/plans.json @@ -0,0 +1,532 @@ +{ + "dataset_name": "Dataset012_OAR_TV", + "plans_name": "nnUNetPlans", + "original_median_spacing_after_transp": [ + 1.25, + 0.5859379768371582, + 0.5859379768371582 + ], + "original_median_shape_after_transp": [ + 159, + 497, + 505 + ], + "image_reader_writer": "SimpleITKIO", + "transpose_forward": [ + 0, + 1, + 2 + ], + "transpose_backward": [ + 0, + 1, + 2 + ], + "configurations": { + "2d": { + "data_identifier": "nnUNetPlans_2d", + "preprocessor_name": "DefaultPreprocessor", + "batch_size": 12, + "patch_size": [ + 512, + 512 + ], + "median_image_size_in_voxels": [ + 497.0, + 505.0 + ], + "spacing": [ + 0.5859379768371582, + 0.5859379768371582 + ], + "normalization_schemes": [ + "ZScoreNormalization" + ], + "use_mask_for_norm": [ + true + ], + "resampling_fn_data": "resample_data_or_seg_to_shape", + "resampling_fn_seg": "resample_data_or_seg_to_shape", + "resampling_fn_data_kwargs": { + "is_seg": false, + "order": 3, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_seg_kwargs": { + "is_seg": true, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_probabilities": "resample_data_or_seg_to_shape", + "resampling_fn_probabilities_kwargs": { + "is_seg": false, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "architecture": { + "network_class_name": "dynamic_network_architectures.architectures.unet.PlainConvUNet", + "arch_kwargs": { + "n_stages": 8, + "features_per_stage": [ + 32, + 64, + 128, + 256, + 512, + 512, + 512, + 512 + ], + "conv_op": "torch.nn.modules.conv.Conv2d", + "kernel_sizes": [ + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ], + [ + 3, + 3 + ] + ], + "strides": [ + [ + 1, + 1 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ], + [ + 2, + 2 + ] + ], + "n_conv_per_stage": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "n_conv_per_stage_decoder": [ + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ], + "conv_bias": true, + "norm_op": "torch.nn.modules.instancenorm.InstanceNorm2d", + "norm_op_kwargs": { + "eps": 1e-05, + "affine": true + }, + "dropout_op": null, + "dropout_op_kwargs": null, + "nonlin": "torch.nn.LeakyReLU", + "nonlin_kwargs": { + "inplace": true + } + }, + "_kw_requires_import": [ + "conv_op", + "norm_op", + "dropout_op", + "nonlin" + ] + }, + "batch_dice": true + }, + "3d_lowres": { + "data_identifier": "nnUNetPlans_3d_lowres", + "preprocessor_name": "DefaultPreprocessor", + "batch_size": 2, + "patch_size": [ + 64, + 192, + 192 + ], + "median_image_size_in_voxels": [ + 102, + 301, + 306 + ], + "spacing": [ + 1.89073715606889, + 0.9684661976733974, + 0.9684661976733974 + ], + "normalization_schemes": [ + "ZScoreNormalization" + ], + "use_mask_for_norm": [ + true + ], + "resampling_fn_data": "resample_data_or_seg_to_shape", + "resampling_fn_seg": "resample_data_or_seg_to_shape", + "resampling_fn_data_kwargs": { + "is_seg": false, + "order": 3, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_seg_kwargs": { + "is_seg": true, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_probabilities": "resample_data_or_seg_to_shape", + "resampling_fn_probabilities_kwargs": { + "is_seg": false, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "architecture": { + "network_class_name": "dynamic_network_architectures.architectures.unet.PlainConvUNet", + "arch_kwargs": { + "n_stages": 6, + "features_per_stage": [ + 32, + 64, + 128, + 256, + 320, + 320 + ], + "conv_op": "torch.nn.modules.conv.Conv3d", + "kernel_sizes": [ + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ] + ], + "strides": [ + [ + 1, + 1, + 1 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 1, + 2, + 2 + ] + ], + "n_conv_per_stage": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "n_conv_per_stage_decoder": [ + 2, + 2, + 2, + 2, + 2 + ], + "conv_bias": true, + "norm_op": "torch.nn.modules.instancenorm.InstanceNorm3d", + "norm_op_kwargs": { + "eps": 1e-05, + "affine": true + }, + "dropout_op": null, + "dropout_op_kwargs": null, + "nonlin": "torch.nn.LeakyReLU", + "nonlin_kwargs": { + "inplace": true + } + }, + "_kw_requires_import": [ + "conv_op", + "norm_op", + "dropout_op", + "nonlin" + ] + }, + "batch_dice": false, + "next_stage": "3d_cascade_fullres" + }, + "3d_fullres": { + "data_identifier": "nnUNetPlans_3d_fullres", + "preprocessor_name": "DefaultPreprocessor", + "batch_size": 2, + "patch_size": [ + 56, + 160, + 192 + ], + "median_image_size_in_voxels": [ + 155.0, + 497.0, + 505.0 + ], + "spacing": [ + 1.25, + 0.5859379768371582, + 0.5859379768371582 + ], + "normalization_schemes": [ + "ZScoreNormalization" + ], + "use_mask_for_norm": [ + true + ], + "resampling_fn_data": "resample_data_or_seg_to_shape", + "resampling_fn_seg": "resample_data_or_seg_to_shape", + "resampling_fn_data_kwargs": { + "is_seg": false, + "order": 3, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_seg_kwargs": { + "is_seg": true, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "resampling_fn_probabilities": "resample_data_or_seg_to_shape", + "resampling_fn_probabilities_kwargs": { + "is_seg": false, + "order": 1, + "order_z": 0, + "force_separate_z": null + }, + "architecture": { + "network_class_name": "dynamic_network_architectures.architectures.unet.PlainConvUNet", + "arch_kwargs": { + "n_stages": 6, + "features_per_stage": [ + 32, + 64, + 128, + 256, + 320, + 320 + ], + "conv_op": "torch.nn.modules.conv.Conv3d", + "kernel_sizes": [ + [ + 1, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ], + [ + 3, + 3, + 3 + ] + ], + "strides": [ + [ + 1, + 1, + 1 + ], + [ + 1, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 2, + 2, + 2 + ], + [ + 1, + 2, + 2 + ] + ], + "n_conv_per_stage": [ + 2, + 2, + 2, + 2, + 2, + 2 + ], + "n_conv_per_stage_decoder": [ + 2, + 2, + 2, + 2, + 2 + ], + "conv_bias": true, + "norm_op": "torch.nn.modules.instancenorm.InstanceNorm3d", + "norm_op_kwargs": { + "eps": 1e-05, + "affine": true + }, + "dropout_op": null, + "dropout_op_kwargs": null, + "nonlin": "torch.nn.LeakyReLU", + "nonlin_kwargs": { + "inplace": true + } + }, + "_kw_requires_import": [ + "conv_op", + "norm_op", + "dropout_op", + "nonlin" + ] + }, + "batch_dice": true + }, + "3d_cascade_fullres": { + "inherits_from": "3d_fullres", + "previous_stage": "3d_lowres" + } + }, + "experiment_planner_used": "ExperimentPlanner", + "label_manager": "LabelManager", + "foreground_intensity_properties_per_channel": { + "0": { + "max": 13339.3759765625, + "mean": 534.5079307033803, + "median": 364.7799987792969, + "min": -3802.36328125, + "percentile_00_5": 0.0, + "percentile_99_5": 3067.902587890625, + "std": 528.3204154692955 + } + } +} \ No newline at end of file diff --git a/onlylian/onlylian/predict_from_raw_data_args.json b/onlylian/onlylian/predict_from_raw_data_args.json new file mode 100755 index 0000000..85b86de --- /dev/null +++ b/onlylian/onlylian/predict_from_raw_data_args.json @@ -0,0 +1,11 @@ +{ + "folder_with_segs_from_prev_stage": null, + "list_of_lists_or_source_folder": "/nn/7208298/20241213/input", + "num_parts": 1, + "num_processes_preprocessing": 3, + "num_processes_segmentation_export": 3, + "output_folder_or_list_of_truncated_output_files": "/nn/7208298/20241213/output", + "overwrite": true, + "part_id": 0, + "save_probabilities": false +} \ No newline at end of file diff --git a/onlylian/registration b/onlylian/registration new file mode 120000 index 0000000..9ec1223 --- /dev/null +++ b/onlylian/registration @@ -0,0 +1 @@ +../registration/ \ No newline at end of file diff --git a/onlylian/test2.py b/onlylian/test2.py new file mode 100755 index 0000000..6bb2e45 --- /dev/null +++ b/onlylian/test2.py @@ -0,0 +1,193 @@ + + +import surfa as sf + +ct = sf.load_volume('0/ct.nii.gz') +clipped = ct.clip(0, 80) +clipped.save('0/clipped.nii.gz') + +exit() + + +from typing import Optional + +import os + +import SimpleITK as sitk + +from pyradise.data import (Subject, IntensityImage, SegmentationImage, + Modality, Organ, Annotator) +from pyradise.fileio.extraction import SimpleModalityExtractor +import pyradise.data as ps_data +import pyradise.fileio as ps_io + +# from nii_to_dicom import convert_subject_to_dicom_rtss + +DCM_CT = '/nn/7295866/20250127/CT/a' +label_file = '/nn/7295866/20250127/output/processed_7_3D_SAG_T1_MPRAGE_+C_20250127132612.nii.gz' + + +segdir = '/nn/7295866/20250127/seg' +os.makedirs(segdir, exist_ok=True) + +output_dir_path = '/nn/7295866/20250127/pyradise' +os.makedirs(output_dir_path, exist_ok=True) + +rtss_file = '/nn/7295866/20250127/CT/a-rtss2.dcm' + + +class ExampleModalityExtractor(ps_io.ModalityExtractor): + + def extract_from_dicom(self, + path: str + ) -> Optional[ps_data.Modality]: + # Extract the necessary attributes from the DICOM file + tags = (ps_io.Tag((0x0008, 0x0060)), # Modality + ps_io.Tag((0x0008, 0x103e))) # Series Description + dataset_dict = self._load_dicom_attributes(tags, path) + + # Identify the modality rule-based + modality = dataset_dict.get('Modality', {}).get('value', None) + series_desc = dataset_dict.get('Series Description', {}).get('value', '') + if modality == 'MR': + if 't1' in series_desc.lower(): + return ps_data.Modality('T1') + elif 't2' in series_desc.lower(): + return ps_data.Modality('T2') + else: + return None + elif modality == 'CT': + return ps_data.Modality('CT') + else: + return None + + def extract_from_path(self, + path: str + ) -> Optional[ps_data.Modality]: + + if 'CT' in path: + return ps_data.Modality('CT') + + # Identify the discrete image file's modality rule-based + filename = os.path.basename(path) + + # Check if the image contains an img prefix + # (i.e., it is a intensity image) + if not filename.startswith('img'): + return None + + # Check if the image contains a modality search string + if 'T1' in filename: + return ps_data.Modality('T1') + elif 'T2' in filename: + return ps_data.Modality('T2') + else: + return None + + + +reader = sitk.ImageSeriesReader() +dicom_names = reader.GetGDCMSeriesFileNames(DCM_CT) +reader.SetFileNames(dicom_names) +reader.MetaDataDictionaryArrayUpdateOn() +reader.LoadPrivateTagsOn() +image = reader.Execute() + + +final_image = sitk.ReadImage(label_file) +final_image = sitk.Resample( + final_image, + image, + sitk.Transform(), + sitk.sitkNearestNeighbor +) + +lss = sitk.LabelShapeStatisticsImageFilter() +lss.Execute(final_image) + +images = [] +images.append(IntensityImage(image, Modality('CT'))) + + +for label in lss.GetLabels(): + segfile = os.path.join(segdir, 'seg-%d.nii.gz'%label) + print(segfile) +# sitk.WriteImage(final_image==label, segfile) + images.append(SegmentationImage(final_image==label, str(label))) + +subject = Subject('7295866', images) + +dcm_crawler = ps_io.SubjectDicomCrawler(DCM_CT, +# modality_extractor=SimpleModalityExtractor(['CT']), + modality_extractor=ExampleModalityExtractor(), + ) +dicom_series_info = dcm_crawler.execute() + +print(dicom_series_info[0]) +print(dicom_series_info[0].get_modality()) +print(dicom_series_info[0].get_path()) +print(dicom_series_info[0].get_patient_id()) +print(dicom_series_info[0].get_patient_name()) + + +# exit() + +reference_modality = 'CT' + +use_3d_conversion = True +if use_3d_conversion: + conv_conf = ps_io.RTSSConverter3DConfiguration() +else: + conv_conf = ps_io.RTSSConverter2DConfiguration() + + +meta_data = ps_io.RTSSMetaData( +# patient_size='180', +# patient_weight='80', +# patient_age='050Y', + series_description='Converted from NIfTI') + + +converter = ps_io.SubjectToRTSSConverter(subject, + dicom_series_info, + reference_modality, + conv_conf, + meta_data) +rtss = converter.convert() + + +# Write the DICOM-RTSS to a separate subject directory +# and include the DICOM files crawled before +# Note: If you want to output just a subset of the +# original DICOM files you may use additional selectors + +writer = ps_io.DicomSeriesSubjectWriter() +rtss_filename = rtss_file +rtss_combination = ((rtss_filename, rtss),) + +writer.write(rtss_combination, output_dir_path, + subject.get_name(), dicom_series_info) + + +''' +pip install git+https://github.com/adalca/neurite.git git+https://github.com/freesurfer/surfa.git git+https://github.com/voxelmorph/voxelmorph.git + + + +mri_synthmorph/mri_synthmorph '/nn/7295866/20250127/output/processed_7_3D_SAG_T1_MPRAGE_+C_20250127132612.nii.gz' -m affine -t trans.lta -o out-aff.nii.gz moving.nii.gz clipped.nii.gz -g + + +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447_5.nii.gz' '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz' -g + + +###models +wget https://surfer.nmr.mgh.harvard.edu/docs/synthmorph/synthmorph.affine.2.h5 +wget https://surfer.nmr.mgh.harvard.edu/docs/synthmorph/synthmorph.deform.3.h5 +wget https://surfer.nmr.mgh.harvard.edu/docs/synthmorph/synthmorph.rigid.1.h5 + +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz t1c.nii.gz ct.nii.gz -g + +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -m affine -t 0/trans.lta -o 0/out-aff.nii.gz 0/t1c.nii.gz 0/clipped.nii.gz -g +time FREESURFER_HOME=/123/onlylian/mri_synthmorph mri_synthmorph/mri_synthmorph -o 0/out.nii.gz 0/t1c.nii.gz 0/clipped.nii.gz -g + +''' \ No newline at end of file diff --git a/pxrjso6b.mat b/pxrjso6b.mat deleted file mode 100644 index ebe919e..0000000 Binary files a/pxrjso6b.mat and /dev/null differ diff --git a/razmtoz2.mat b/razmtoz2.mat deleted file mode 100644 index 992d9c6..0000000 Binary files a/razmtoz2.mat and /dev/null differ diff --git a/registration/Untitled.ipynb b/registration/Untitled.ipynb new file mode 100755 index 0000000..853a6a5 --- /dev/null +++ b/registration/Untitled.ipynb @@ -0,0 +1,102 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 8, + "id": "82327832", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3.141592653589793\n", + "[[ 0.65011138 -0.75983893 0. ]\n", + " [ 0.75983893 0.65011138 0. ]\n", + " [ 0. 0. 1. ]]\n" + ] + } + ], + "source": [ + "from math import sin, cos\n", + "import numpy as np\n", + "\n", + "print(np.pi)\n", + "\n", + "a=49.45*np.pi/180\n", + "b=11.31*np.pi/180\n", + "c=-2.11*np.pi/180\n", + "\n", + "yaw = np.array([\n", + " [cos(a), -sin(a), 0],\n", + " [sin(a), cos(a), 0],\n", + " [0,0,1]\n", + "])\n", + "pitch = np.array([\n", + " [cos(b), 0, sin(b)],\n", + " [0, 1, 0],\n", + " [-sin(b),0,cos(b)]\n", + "])\n", + "roll = np.array([\n", + " [1, 0, 0],\n", + " [0, cos(c), -sin(c)],\n", + " [0,sin(c),cos(c)]\n", + "])\n", + "\n", + "R=yaw@pitch@roll\n", + "print(yaw)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ac13f413", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.8414709848078965" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import math\n", + "math.sin(1)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b656fc03", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/registration/__init__.py b/registration/__init__.py old mode 100644 new mode 100755 diff --git a/registration/ants_reg.py b/registration/ants_reg.py old mode 100644 new mode 100755 index dbc75fa..5e7593d --- a/registration/ants_reg.py +++ b/registration/ants_reg.py @@ -10,7 +10,11 @@ import ants class ants_reg: def register_aux(self, fi, mv): - mytx = ants.registration(fixed=fi, moving=mv, type_of_transform = 'Rigid', + mytx = ants.registration( + fixed=fi, + moving=mv, +# type_of_transform = 'Rigid', + type_of_transform = 'SyNRA', # verbose=True, ) @@ -25,7 +29,17 @@ class ants_reg: return { 'fwdtransforms': fwdtransforms, - 'invtransforms': fwdtransforms.invert(), +# 'invtransforms': fwdtransforms.invert(), +# inverseTransform(): incompatible function arguments. The following argument types are supported: +# 1. inverseTransform(arg: ants.lib.AntsTransformF22, /) -> ants.lib.AntsTransformF22 +# 2. inverseTransform(arg: ants.lib.AntsTransformF33, /) -> ants.lib.AntsTransformF33 +# 3. inverseTransform(arg: ants.lib.AntsTransformF44, /) -> ants.lib.AntsTransformF44 +# 4. inverseTransform(arg: ants.lib.AntsTransformD22, /) -> ants.lib.AntsTransformD22 +# 5. inverseTransform(arg: ants.lib.AntsTransformD33, /) -> ants.lib.AntsTransformD33 +# 6. inverseTransform(arg: ants.lib.AntsTransformD44, /) -> ants.lib.AntsTransformD44 + +# Invoked with types: ants.lib.AntsTransformDF3 + 'warpedfixout': mytx['warpedfixout'], 'warpedmovout': mytx['warpedmovout'], 'metrics': max(m1, m2) @@ -43,7 +57,7 @@ class ants_reg: else: self.res = dict(r2) self.res.update({ - 'fwdtransforms': r2['invtransforms'], +# 'fwdtransforms': r2['invtransforms'], 'invtransforms': r2['fwdtransforms'], 'warpedfixout': r2['warpedmovout'], 'warpedmovout': r2['warpedfixout'], @@ -66,6 +80,10 @@ class ants_reg: def get_metrics(self): return self.res['metrics'] + def write_warpedmovout(self, out): + ants.image_write(self.res['warpedmovout'], out) + + def transform(self, moving, output_filename, is_label=False): transform1 = next(tempfile._get_candidate_names())+'.mat' # print(transform1) diff --git a/registration/best_reg.py b/registration/best_reg.py old mode 100644 new mode 100755 index c2e9459..644ef02 --- a/registration/best_reg.py +++ b/registration/best_reg.py @@ -9,15 +9,28 @@ from .elastix_reg import elastix_reg Q = multiprocessing.Queue() -def registration(method, fix, mov_image, mov_label): +''' +if mov_label is not None, return transformed label, else return transformed images +''' + + +def registration(method, fix, mov_image, mov_label=None): start = time.time() transformed = next(tempfile._get_candidate_names())+'.nii.gz' r = method(fix, mov_image) - r.transform(mov_label, transformed, is_label=False) - r.transform(mov_label, transformed, is_label=True) + + if mov_label is None: + r.write_warpedmovout(transformed) +# print(r) +# shutil.copy(r.get_warpedmovout(), transformed) + else: +# r.transform(mov_label, transformed, is_label=False) + r.transform(mov_label, transformed, is_label=True) end = time.time() + +# print(r) res = { 'name': r.__class__.__name__, @@ -48,21 +61,46 @@ def reg_transform(fix, mov_image, mov_label, out_label): rlist = dump_queue(Q) # print(rlist) rlist2 = sorted(rlist, key=lambda r: -r['metrics']) - print(rlist2) +# print(rlist2) shutil.copy(rlist2[0]['transformed'], out_label) for r in rlist2: os.remove(r['transformed']) +def reg_only(fix, mov_image, out_image): + regs = [ants_reg, elastix_reg] + + inputs = [(r, fix, mov_image) for r in regs] + pool = multiprocessing.Pool(4) + # print(inputs) + pool_outputs = pool.starmap(registration, inputs) + pool.close() + + print(pool_outputs) + rlist = dump_queue(Q) +# print(rlist) + rlist2 = sorted(rlist, key=lambda r: -r['metrics']) +# print(rlist2) + + shutil.copy(rlist2[0]['transformed'], out_image) + + for r in rlist2: + os.remove(r['transformed']) + + fi = '/nn/2896833/20220506/nii/b_C+MAR_20220506155936_301.nii.gz' mv_img = '/nn/2896833/20220506/nii/9_3D_fl3d_mt_FS_+_c_MPR_Tra_20220506142416_15.nii.gz' mv_lab = '/nn/2896833/20220506/output/9_3D_fl3d_mt_FS_+_c_MPR_Tra_20220506142416.nii.gz' +fi = '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447_5.nii.gz' +mv_img = '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz' if __name__ == '__main__': - reg_transform(fi, mv_img, mv_lab, 'tmp.nii.gz') +# reg_transform(fi, mv_img, mv_lab, 'tmp.nii.gz') + + reg_only(fi, mv_img, 'tmp.nii.gz') # regs = [ants_reg, elastix_reg] diff --git a/registration/elastix_reg.py b/registration/elastix_reg.py old mode 100644 new mode 100755 index 29d33d2..16820d2 --- a/registration/elastix_reg.py +++ b/registration/elastix_reg.py @@ -105,7 +105,9 @@ class elastix_reg: last_parameter_map = params2.GetParameterMap(0) # pprint(last_parameter_map.asdict()) # exit() +# WARNING: The parameter name "InitialTransformParametersFileName" is deprecated. Please use "InitialTransformParameterFileName" (without letter 's') instead. last_parameter_map["InitialTransformParametersFileName"] = ["NoInitialTransform"] + last_parameter_map["InitialTransformParameterFileName"] = ["NoInitialTransform"] params2.SetParameterMap(0, last_parameter_map) # params2.WriteParameterFile('123.txt') @@ -140,6 +142,7 @@ class elastix_reg: # METRIC_THRESHOLD = 1.1 def __init__(self, fi, mv, warpedfixout=None, warpedmovout=None, debug=False, iterations_init=NumberOfIterations): + self.debug = debug # reader = itk.ImageFileReader[ImageType].New() # reader.SetFileName(fi) @@ -226,6 +229,9 @@ class elastix_reg: def get_metrics(self): return self.res['metrics'] + def write_warpedmovout(self, out): + itk.imwrite(self.res['warpedmovout'], out) + def transform(self, moving, output_filename, is_label=False): transform1 = self.res['fwdtransforms'] mv = itk.imread(moving) @@ -242,7 +248,9 @@ class elastix_reg: output = itk.transformix_filter( mv.astype(itk.F), - t2) + t2, + log_to_console=self.debug, + ) if is_label: output=output.astype(itk.UC) diff --git a/registration/fireants_reg.py b/registration/fireants_reg.py new file mode 100755 index 0000000..ac2bdc1 --- /dev/null +++ b/registration/fireants_reg.py @@ -0,0 +1,157 @@ +from pprint import pprint +from time import time + +import os +import tempfile + +from skimage.metrics import normalized_mutual_information + +from fireants.io import Image, BatchedImages +from fireants.registration import RigidRegistration + +import SimpleITK as sitk + +class fireants_reg: + + def register_aux(self, fi, mv): + mytx = ants.registration( + fixed=fi, + moving=mv, +# type_of_transform = 'Rigid', + type_of_transform = 'SyNRA', + # verbose=True, + ) + +# print(mytx['fwdtransforms'][0]) + fwdtransforms = ants.read_transform(mytx['fwdtransforms'][0]) + + m1 = normalized_mutual_information(fi.numpy(), mytx['warpedmovout'].numpy()) + m2 = normalized_mutual_information(mv.numpy(), mytx['warpedfixout'].numpy()) + + + print(m1, m2) + + return { + 'fwdtransforms': fwdtransforms, +# 'invtransforms': fwdtransforms.invert(), +# inverseTransform(): incompatible function arguments. The following argument types are supported: +# 1. inverseTransform(arg: ants.lib.AntsTransformF22, /) -> ants.lib.AntsTransformF22 +# 2. inverseTransform(arg: ants.lib.AntsTransformF33, /) -> ants.lib.AntsTransformF33 +# 3. inverseTransform(arg: ants.lib.AntsTransformF44, /) -> ants.lib.AntsTransformF44 +# 4. inverseTransform(arg: ants.lib.AntsTransformD22, /) -> ants.lib.AntsTransformD22 +# 5. inverseTransform(arg: ants.lib.AntsTransformD33, /) -> ants.lib.AntsTransformD33 +# 6. inverseTransform(arg: ants.lib.AntsTransformD44, /) -> ants.lib.AntsTransformD44 + +# Invoked with types: ants.lib.AntsTransformDF3 + + 'warpedfixout': mytx['warpedfixout'], + 'warpedmovout': mytx['warpedmovout'], + 'metrics': max(m1, m2) + } + + def __init__(self, fi, mv, debug=False): + fixed_image = ants.image_read(fi, dimension=3) + moving_image = ants.image_read(mv, dimension=3) + r1 = self.register_aux(fixed_image, moving_image) + r2 = self.register_aux(moving_image, fixed_image) + + + if r1['metrics'] > r2['metrics']: + self.res = r1 + else: + self.res = dict(r2) + self.res.update({ +# 'fwdtransforms': r2['invtransforms'], + 'invtransforms': r2['fwdtransforms'], + 'warpedfixout': r2['warpedmovout'], + 'warpedmovout': r2['warpedfixout'], + }) + self.res.update({ + 'fix': fixed_image, + 'mov': moving_image, + }) + + + if debug: + pprint(self.res) + ants.image_write(fixed_image, '0fixed.nii.gz') + ants.image_write(moving_image, '0moving.nii.gz') + ants.image_write(r1['warpedfixout'], '0mf1.nii.gz') + ants.image_write(r1['warpedmovout'], '0fm1.nii.gz') + ants.image_write(r2['warpedmovout'], '0mf2.nii.gz') + ants.image_write(r2['warpedfixout'], '0fm2.nii.gz') + + def get_metrics(self): + return self.res['metrics'] + + def write_warpedmovout(self, out): + ants.image_write(self.res['warpedmovout'], out) + + + def transform(self, moving, output_filename, is_label=False): + transform1 = next(tempfile._get_candidate_names())+'.mat' + # print(transform1) + ants.write_transform(self.res['fwdtransforms'], transform1) + mi = ants.image_read(moving, dimension=3) + if is_label: + transformed = ants.apply_transforms(self.res['fix'], mi, + transformlist=[transform1], interpolator='genericLabel').astype('uint8') + else: + transformed = ants.apply_transforms(self.res['fix'], mi, + transformlist=[transform1]) + # print(transformed) + ants.image_write(transformed, output_filename) + os.remove(transform1) + +if __name__ == '__main__': + + fi = '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447_5.nii.gz' + mv = '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz' + + # load the images + image1 = Image.load_file(fi) + image2 = Image.load_file(mv) + # batchify them (we only have a single image per batch, but we can pass multiple images) + fixed_batch = BatchedImages([image1]) + moving_batch = BatchedImages([image2]) + + # rigid registration + scales = [4, 2, 1] # scales at which to perform registration + iterations = [200, 100, 50] + scales = [4, 2] # scales at which to perform registration + iterations = [200, 100] + optim = 'Adam' + lr = 3e-4 + + # create rigid registration object + rigid_reg = RigidRegistration( + scales, iterations, fixed_batch, moving_batch, + loss_type = 'mi', +# mi_kernel_type = 'gaussian', +# optimizer=optim, optimizer_lr=lr, +# cc_kernel_size=5, + ) + # call method +# rigid_reg.optimize() + + start = time() + rigid_reg.optimize(save_transformed=False) + end = time() + + print("Runtime", end - start, "seconds") + + moved = rigid_reg.evaluate(fixed_batch, moving_batch) + + reference_img = sitk.ReadImage(fi) + + # Preparing the moving image to be written out + moved_image_np = moved[0, 0].detach().cpu().numpy() # volumes are typically stored in tensors with dimensions [Batch, Channels, Depth, Height, Width], so extracting the latter 3 for nifti + moved_sitk_image = sitk.GetImageFromArray(moved_image_np) + moved_sitk_image.SetOrigin(reference_img.GetOrigin()) + moved_sitk_image.SetSpacing(reference_img.GetSpacing()) + moved_sitk_image.SetDirection(reference_img.GetDirection()) + sitk.WriteImage(moved_sitk_image, 'tmp.nii.gz') +# reg_only(fi, mv_img, 'tmp.nii.gz') + + + \ No newline at end of file diff --git a/test.md b/test.md new file mode 100755 index 0000000..a7f7785 --- /dev/null +++ b/test.md @@ -0,0 +1,24 @@ +# Add user + +## DC: + +``` +export USERNAME=wjhuang +export PASSWORD=`tr -dc A-Za-z Optional[ps_data.Modality]: + # Extract the necessary attributes from the DICOM file + tags = (ps_io.Tag((0x0008, 0x0060)), # Modality + ps_io.Tag((0x0008, 0x103e))) # Series Description + dataset_dict = self._load_dicom_attributes(tags, path) + + # Identify the modality rule-based + modality = dataset_dict.get('Modality', {}).get('value', None) + series_desc = dataset_dict.get('Series Description', {}).get('value', '') + if modality == 'MR': + if 't1' in series_desc.lower(): + return ps_data.Modality('T1') + elif 't2' in series_desc.lower(): + return ps_data.Modality('T2') + else: + return None + else: + return None + + def extract_from_path(self, + path: str + ) -> Optional[ps_data.Modality]: + # Identify the discrete image file's modality rule-based + filename = os.path.basename(path) + + # Check if the image contains an img prefix + # (i.e., it is a intensity image) + if not filename.startswith('img'): + return None + + # Check if the image contains a modality search string + if 'T1' in filename: + return ps_data.Modality('T1') + elif 'T2' in filename: + return ps_data.Modality('T2') + else: + return None + + +class ExampleOrganExtractor(ps_io.OrganExtractor): + + def extract(self, + path: str + ) -> Optional[ps_data.Organ]: + # Identify the discrete image file's organ rule-based + filename = os.path.basename(path) + + # Check if the image contains a seg prefix + # (i.e., it is a segmentation) + if not filename.startswith('seg'): + return None + + # Split the filename for extracting the organ name + organ_name = filename.split('_')[-1].split('.')[0] + return ps_data.Organ(organ_name) + + +class ExampleAnnotatorExtractor(ps_io.AnnotatorExtractor): + + def extract(self, + path: str + ) -> Optional[ps_data.Annotator]: + # Identify the discrete image file's annotator rule-based + filename = os.path.basename(path) + + # Check if the image contains a seg prefix + # (i.e., it is a segmentation) + if not filename.startswith('seg'): + return None + + # Split the filename for extracting the annotator name + annotator_name = filename.split('_')[2] + return ps_data.Annotator(annotator_name) + + +def convert_subject_to_dicom_rtss(input_dir_path: str, + output_dir_path: str, + dicom_image_dir_path: str, + use_3d_conversion: bool = True + ) -> None: + # Specify a reference modalities + # This is the modality of the DICOM image series that will be + # referenced in the DICOM-RTSS. + reference_modality = 'T1' + + # Create the loader + loader = ps_io.SubjectLoader() + + # Create the writer and specify the output file name of the + # DICOM-RTSS files + writer = ps_io.DicomSeriesSubjectWriter() + rtss_filename = 'rtss.dcm' + + # (optional) + # Instantiate a new selection to exclude the original DICOM-RTSS SeriesInfo + # Note: If this is omitted the original DICOM-RTSS will be copied to the + # corresponding output directory. + selection = ps_io.NoRTSSInfoSelector() + + # Create the file crawler for the discrete image files and + # loop through the subjects + crawler = ps_io.DatasetFileCrawler(input_dir_path, + extension='.nii.gz', + modality_extractor=ExampleModalityExtractor(), + organ_extractor=ExampleOrganExtractor(), + annotator_extractor=ExampleAnnotatorExtractor()) + for series_info in crawler: + # Load the subject + subject = loader.load(series_info) + + # Print the progress + print(f'Converting subject {subject.get_name()}...') + + # Construct the path to the subject's DICOM images + dicom_subject_path = os.path.join(dicom_image_dir_path, subject.get_name()) + + # Construct a DICOM crawler to retrieve the reference + # DICOM image series info + dcm_crawler = ps_io.SubjectDicomCrawler(dicom_subject_path, + modality_extractor=ExampleModalityExtractor()) + dicom_series_info = dcm_crawler.execute() + + # (optional) + # Keep all SeriesInfo entries that do not describe a DICOM-RTSS for loading + dicom_series_info = selection.execute(dicom_series_info) + + # (optional) + # Define the metadata for the DICOM-RTSS + # Note: For some attributes, the value must follow the value + # representation of the DICOM standard. + meta_data = ps_io.RTSSMetaData(patient_size='180', + patient_weight='80', + patient_age='050Y', + series_description='Converted from NIfTI') + + # Convert the segmentations to a DICOM-RTSS with standard smoothing settings. + # For the conversion we can either use a 2D or a 3D algorithm (see API reference + # for details). + # Note: Inappropriate smoothing leads to corrupted structures if their size + # is too small + if use_3d_conversion: + conv_conf = ps_io.RTSSConverter3DConfiguration() + else: + conv_conf = ps_io.RTSSConverter2DConfiguration() + + converter = ps_io.SubjectToRTSSConverter(subject, + dicom_series_info, + reference_modality, + conv_conf, + meta_data) + rtss = converter.convert() + + # Combine the DICOM-RTSS with its output file name + rtss_combination = ((rtss_filename, rtss),) + + # Write the DICOM-RTSS to a separate subject directory + # and include the DICOM files crawled before + # Note: If you want to output just a subset of the + # original DICOM files you may use additional selectors + writer.write(rtss_combination, output_dir_path, + subject.get_name(), dicom_series_info) + + +# The indicator if the 2D or the 3D conversion algorithm should +# be used. +use_3d_algorithm = True + +# The input path pointing to the top-level directory containing the +# NIfTI subject directories +input_dataset_path = '//YOUR/PATH/TO/THE/EXAMPLE/DATA/nifti_data' +input_dataset_path = '/nn/3378417/20241112/output/7_AX_3D_fl3d_mt_FS_+_c_20241112142646.nii.gz' +input_dataset_path = '/nn/3378417/20241112/output' + +# The input path pointing to the top-level directory containing the +# DICOM subject directories that will get referenced in the output +# DICOM-RTSS files +dicom_dataset_path = '//YOUR/PATH/TO/THE/EXAMPLE/DATA/dicom_data' +dicom_dataset_path = '/nn/3378417/20241112/CT/8' + +# The output path pointing to an empty directory where the output +# will be saved +output_dataset_path = '//YOUR/PATH/TO/THE/OUTPUT/DIRECTORY/' +output_dataset_path = '0' + +# Execution of the conversion procedure +convert_subject_to_dicom_rtss(input_dataset_path, + output_dataset_path, + dicom_dataset_path, + use_3d_algorithm)