Add neck-based CT prealignment before VoxelMorph
This commit is contained in:
30
README.md
30
README.md
@@ -25,7 +25,8 @@ Voxelmorph_Head_CT/
|
|||||||
├── infer.py # 独立推理,输出 warped image 与 DDF
|
├── infer.py # 独立推理,输出 warped image 与 DDF
|
||||||
├── metrics.py # NCC/MSE/MAE/DDF 等量化指标
|
├── metrics.py # NCC/MSE/MAE/DDF 等量化指标
|
||||||
├── model_and_train.py # 官方 VoxelMorph 训练适配器
|
├── model_and_train.py # 官方 VoxelMorph 训练适配器
|
||||||
├── preprocess.py # 重采样、窗宽窗位、裁剪/填充
|
├── prealign.py # 颈部基准 CT 预配准
|
||||||
|
├── preprocess.py # 单体 CT 重采样、窗宽窗位、裁剪/填充
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
└── outputs/
|
└── outputs/
|
||||||
├── nifti/ # DICOM 转换结果
|
├── nifti/ # DICOM 转换结果
|
||||||
@@ -69,23 +70,20 @@ python data_loader.py \
|
|||||||
|
|
||||||
`data_loader.py` 会优先按 `InstanceNumber` 排序,其次按 `SliceLocation` 排序,并保存 spacing、层厚、排序依据等元数据 JSON。
|
`data_loader.py` 会优先按 `InstanceNumber` 排序,其次按 `SliceLocation` 排序,并保存 spacing、层厚、排序依据等元数据 JSON。
|
||||||
|
|
||||||
## 2. 预处理
|
## 2. 颈部预配准
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python preprocess.py \
|
python prealign.py \
|
||||||
--input "outputs/nifti/patient1_fixed.nii.gz" \
|
--fixed "outputs/nifti/patient1_fixed.nii.gz" \
|
||||||
--output "outputs/preprocessed/patient1_fixed_preprocessed.nii.gz" \
|
--moving "outputs/nifti/patient1_moving.nii.gz" \
|
||||||
|
--fixed-output "outputs/preprocessed/patient1_fixed_preprocessed.nii.gz" \
|
||||||
|
--moving-output "outputs/preprocessed/patient1_moving_preprocessed.nii.gz" \
|
||||||
--target-spacing 1 1 1 \
|
--target-spacing 1 1 1 \
|
||||||
--target-shape 256 256 352
|
--target-shape 320 320 352 \
|
||||||
|
--metadata-json "outputs/preprocessed/patient1_neck_alignment.json"
|
||||||
python preprocess.py \
|
|
||||||
--input "outputs/nifti/patient1_moving.nii.gz" \
|
|
||||||
--output "outputs/preprocessed/patient1_moving_preprocessed.nii.gz" \
|
|
||||||
--target-spacing 1 1 1 \
|
|
||||||
--target-shape 256 256 352
|
|
||||||
```
|
```
|
||||||
|
|
||||||
默认窗口为 `W=400, L=40`,适合观察颈部软组织和气道。
|
该步骤会先把 Fixed/Moving 重采样到 1mm,再基于颈部 foreground mask 估计中心,并将 Moving 平移到 Fixed 的颈部中心,最后在共同颈部空间中裁剪/填充和归一化。默认窗口为 `W=400, L=40`,适合观察颈部软组织和气道。
|
||||||
|
|
||||||
## 3. 训练
|
## 3. 训练
|
||||||
|
|
||||||
@@ -121,7 +119,7 @@ python infer.py \
|
|||||||
- `outputs/inference/ddf_mm.nii.gz`
|
- `outputs/inference/ddf_mm.nii.gz`
|
||||||
- `outputs/inference/metrics.json`
|
- `outputs/inference/metrics.json`
|
||||||
|
|
||||||
`moving_model_input.nii.gz` 和 `fixed_model_input.nii.gz` 是进入 VoxelMorph 前的统一网格图像。即使两套 CT 原始层数不同,推理前也会按 checkpoint 的输入尺寸和目标 spacing 完成重采样、归一化、中心裁剪/填充。
|
`moving_model_input.nii.gz` 和 `fixed_model_input.nii.gz` 是进入 VoxelMorph 前的统一网格图像。患者1默认先由 `prealign.py` 按颈部中心完成预配准,再进入训练/推理。
|
||||||
|
|
||||||
## 5. Web 结果展示
|
## 5. Web 结果展示
|
||||||
|
|
||||||
@@ -132,7 +130,7 @@ streamlit run app.py
|
|||||||
网页提供:
|
网页提供:
|
||||||
|
|
||||||
- 患者1专用固定路径:Fixed 为 `患者1-平扫CT`,Moving 为 `患者1-仰头CT`。
|
- 患者1专用固定路径:Fixed 为 `患者1-平扫CT`,Moving 为 `患者1-仰头CT`。
|
||||||
- “重新训练模型”和“开始推理”按钮。
|
- “颈部预配准+重新训练”和“开始推理”按钮;推理前也会刷新颈部预配准输入。
|
||||||
- Axial、Coronal、Sagittal 正交三视图;每个平面按行同时展示 Fixed、Moving、Warped。
|
- Axial、Coronal、Sagittal 正交三视图;每个平面按行同时展示 Fixed、Moving、Warped。
|
||||||
- Fixed 与 Warped 的 Alpha 融合或棋盘格对比。
|
- Fixed 与 Warped 的 Alpha 融合或棋盘格对比。
|
||||||
- DDF 位移强度热力图。
|
- DDF 位移强度热力图。
|
||||||
@@ -142,4 +140,4 @@ streamlit run app.py
|
|||||||
|
|
||||||
- DICOM 转换和重采样都有 `--max-memory-mb` 防护。
|
- DICOM 转换和重采样都有 `--max-memory-mb` 防护。
|
||||||
- Web 界面对超大 NIfTI 会通过 nibabel proxy 按 stride 切片读取并下采样,只影响浏览器展示,不改变磁盘结果;侧栏可调整显示体素上限。
|
- Web 界面对超大 NIfTI 会通过 nibabel proxy 按 stride 切片读取并下采样,只影响浏览器展示,不改变磁盘结果;侧栏可调整显示体素上限。
|
||||||
- 训练阶段的主要瓶颈是 3D U-Net 显存;`256x256x352` 是较重的 3D 输入,建议优先使用 CUDA GPU。当前患者1默认使用较轻的 `8 8 8 8 8` 特征配置。
|
- 训练阶段的主要瓶颈是 3D U-Net 显存;`320x320x352` 是较重的 3D 输入,建议优先使用 CUDA GPU。当前患者1默认使用较轻的 `8 8 8 8 8` 特征配置。
|
||||||
|
|||||||
58
app.py
58
app.py
@@ -19,9 +19,12 @@ import streamlit as st
|
|||||||
from config import (
|
from config import (
|
||||||
CHECKPOINT_DIR,
|
CHECKPOINT_DIR,
|
||||||
DEFAULT_CHECKPOINT,
|
DEFAULT_CHECKPOINT,
|
||||||
|
DEFAULT_FIXED_DICOM_DIR,
|
||||||
DEFAULT_FIXED_NIFTI,
|
DEFAULT_FIXED_NIFTI,
|
||||||
|
DEFAULT_MOVING_DICOM_DIR,
|
||||||
DEFAULT_MOVING_NIFTI,
|
DEFAULT_MOVING_NIFTI,
|
||||||
INFERENCE_DIR,
|
INFERENCE_DIR,
|
||||||
|
NIFTI_DIR,
|
||||||
OUTPUT_ROOT,
|
OUTPUT_ROOT,
|
||||||
PROJECT_ROOT,
|
PROJECT_ROOT,
|
||||||
)
|
)
|
||||||
@@ -608,6 +611,37 @@ def run_inference_from_ui(moving_path: str, fixed_path: str, checkpoint_path: st
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_patient1_neck_aligned_inputs() -> Dict:
|
||||||
|
from data_loader import convert_dicom_series_to_nifti
|
||||||
|
from prealign import prealign_pair
|
||||||
|
|
||||||
|
fixed_raw_path = NIFTI_DIR / "patient1_fixed.nii.gz"
|
||||||
|
moving_raw_path = NIFTI_DIR / "patient1_moving.nii.gz"
|
||||||
|
convert_dicom_series_to_nifti(
|
||||||
|
dicom_dir=DEFAULT_FIXED_DICOM_DIR,
|
||||||
|
output_path=fixed_raw_path,
|
||||||
|
max_memory_mb=8192,
|
||||||
|
)
|
||||||
|
convert_dicom_series_to_nifti(
|
||||||
|
dicom_dir=DEFAULT_MOVING_DICOM_DIR,
|
||||||
|
output_path=moving_raw_path,
|
||||||
|
max_memory_mb=8192,
|
||||||
|
)
|
||||||
|
meta = prealign_pair(
|
||||||
|
fixed_input_path=fixed_raw_path,
|
||||||
|
moving_input_path=moving_raw_path,
|
||||||
|
fixed_output_path=DEFAULT_FIXED_NIFTI,
|
||||||
|
moving_output_path=DEFAULT_MOVING_NIFTI,
|
||||||
|
max_memory_mb=8192,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"moving_translation_mm": list(meta.moving_translation_mm),
|
||||||
|
"fixed_neck_center_world_mm": list(meta.fixed_neck_center_world_mm),
|
||||||
|
"moving_neck_center_world_mm": list(meta.moving_neck_center_world_mm),
|
||||||
|
"target_shape_xyz": list(meta.target_shape_xyz),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def run_training_from_ui(moving_path: str, fixed_path: str, checkpoint_path: str) -> None:
|
def run_training_from_ui(moving_path: str, fixed_path: str, checkpoint_path: str) -> None:
|
||||||
from model_and_train import train_pair
|
from model_and_train import train_pair
|
||||||
|
|
||||||
@@ -636,7 +670,7 @@ def main() -> None:
|
|||||||
|
|
||||||
st.caption(
|
st.caption(
|
||||||
"患者1专用:固定图像 = 平扫CT;移动图像 = 仰头CT。"
|
"患者1专用:固定图像 = 平扫CT;移动图像 = 仰头CT。"
|
||||||
"推理前会重采样、归一化并裁剪/填充到同一模型网格。"
|
"先按颈部 foreground 做平移预配准,再进入 VoxelMorph 训练/推理。"
|
||||||
)
|
)
|
||||||
info_cols = st.columns(4)
|
info_cols = st.columns(4)
|
||||||
info_cols[0].metric("Fixed", "患者1-平扫CT")
|
info_cols[0].metric("Fixed", "患者1-平扫CT")
|
||||||
@@ -646,27 +680,31 @@ def main() -> None:
|
|||||||
|
|
||||||
action_cols = st.columns([1, 1, 4])
|
action_cols = st.columns([1, 1, 4])
|
||||||
with action_cols[0]:
|
with action_cols[0]:
|
||||||
train_now = st.button("重新训练模型", type="primary", width="stretch")
|
train_now = st.button("颈部预配准+重新训练", type="primary", width="stretch")
|
||||||
with action_cols[1]:
|
with action_cols[1]:
|
||||||
start = st.button("开始推理", width="stretch")
|
start = st.button("开始推理", width="stretch")
|
||||||
|
|
||||||
if train_now:
|
if train_now:
|
||||||
with st.spinner("患者1模型训练中"):
|
with st.spinner("患者1颈部预配准、模型训练和推理中"):
|
||||||
try:
|
try:
|
||||||
load_nifti_cached.clear()
|
load_nifti_cached.clear()
|
||||||
|
align_info = prepare_patient1_neck_aligned_inputs()
|
||||||
run_training_from_ui(moving_path, fixed_path, checkpoint_path)
|
run_training_from_ui(moving_path, fixed_path, checkpoint_path)
|
||||||
result = run_inference_from_ui(moving_path, fixed_path, checkpoint_path, out_dir)
|
result = run_inference_from_ui(moving_path, fixed_path, checkpoint_path, out_dir)
|
||||||
|
result["neck_alignment"] = align_info
|
||||||
load_nifti_cached.clear()
|
load_nifti_cached.clear()
|
||||||
st.session_state["last_result"] = result
|
st.session_state["last_result"] = result
|
||||||
st.success("训练和推理完成")
|
st.success("颈部预配准、训练和推理完成")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
st.error(str(exc))
|
st.error(str(exc))
|
||||||
|
|
||||||
if start:
|
if start:
|
||||||
with st.spinner("推理运行中"):
|
with st.spinner("颈部预配准和推理运行中"):
|
||||||
try:
|
try:
|
||||||
load_nifti_cached.clear()
|
load_nifti_cached.clear()
|
||||||
|
align_info = prepare_patient1_neck_aligned_inputs()
|
||||||
result = run_inference_from_ui(moving_path, fixed_path, checkpoint_path, out_dir)
|
result = run_inference_from_ui(moving_path, fixed_path, checkpoint_path, out_dir)
|
||||||
|
result["neck_alignment"] = align_info
|
||||||
load_nifti_cached.clear()
|
load_nifti_cached.clear()
|
||||||
st.session_state["last_result"] = result
|
st.session_state["last_result"] = result
|
||||||
st.success("推理完成")
|
st.success("推理完成")
|
||||||
@@ -701,6 +739,16 @@ def main() -> None:
|
|||||||
if result_info and not outputs_current:
|
if result_info and not outputs_current:
|
||||||
st.warning("输出目录中的历史结果与当前输入不匹配,请重新开始推理。")
|
st.warning("输出目录中的历史结果与当前输入不匹配,请重新开始推理。")
|
||||||
|
|
||||||
|
alignment_meta = read_json_dict(Path(DEFAULT_FIXED_NIFTI).parent / "patient1_neck_alignment.json")
|
||||||
|
translation = alignment_meta.get("moving_translation_mm")
|
||||||
|
if isinstance(translation, list) and len(translation) == 3:
|
||||||
|
st.caption(
|
||||||
|
"颈部预配准平移:"
|
||||||
|
f"X={float(translation[0]):+.2f} mm,"
|
||||||
|
f"Y={float(translation[1]):+.2f} mm,"
|
||||||
|
f"Z={float(translation[2]):+.2f} mm"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
moving_xyz, moving_spacing, moving_stride = load_nifti_cached(display_moving_path, max_voxels=display_max_voxels)
|
moving_xyz, moving_spacing, moving_stride = load_nifti_cached(display_moving_path, max_voxels=display_max_voxels)
|
||||||
fixed_xyz, fixed_spacing, fixed_stride = load_nifti_cached(display_fixed_path, max_voxels=display_max_voxels)
|
fixed_xyz, fixed_spacing, fixed_stride = load_nifti_cached(display_fixed_path, max_voxels=display_max_voxels)
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ DEFAULT_MOVING_NIFTI = PREPROCESSED_DIR / "patient1_moving_preprocessed.nii.gz"
|
|||||||
DEFAULT_FIXED_NIFTI = PREPROCESSED_DIR / "patient1_fixed_preprocessed.nii.gz"
|
DEFAULT_FIXED_NIFTI = PREPROCESSED_DIR / "patient1_fixed_preprocessed.nii.gz"
|
||||||
DEFAULT_CHECKPOINT = CHECKPOINT_DIR / "vxm_head_ct_patient1.pt"
|
DEFAULT_CHECKPOINT = CHECKPOINT_DIR / "vxm_head_ct_patient1.pt"
|
||||||
|
|
||||||
# VoxelMorph 的 3D U-Net 多次下采样,三维尺寸建议均为 16 的倍数。
|
# VoxelMorph 的 3D U-Net 多次下采样,三维尺寸建议均为 32 的倍数。
|
||||||
DEFAULT_TARGET_SHAPE = (256, 256, 352) # NIfTI 轴顺序: X, Y, Z
|
DEFAULT_TARGET_SHAPE = (320, 320, 352) # NIfTI 轴顺序: X, Y, Z
|
||||||
DEFAULT_TARGET_SPACING = (1.0, 1.0, 1.0) # mm, X/Y/Z
|
DEFAULT_TARGET_SPACING = (1.0, 1.0, 1.0) # mm, X/Y/Z
|
||||||
|
|
||||||
# 颈部软组织/气道观察常用窗口:W=400, L=40。
|
# 颈部软组织/气道观察常用窗口:W=400, L=40。
|
||||||
|
|||||||
@@ -1,482 +1,482 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"epoch": 1.0,
|
"epoch": 1.0,
|
||||||
"loss": 0.058744531124830246,
|
"loss": 0.04345163702964783,
|
||||||
"image_loss": 0.058744531124830246,
|
"image_loss": 0.04345163702964783,
|
||||||
"smooth_loss": 1.5868747288427798e-12
|
"smooth_loss": 5.719839749410149e-13
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 2.0,
|
"epoch": 2.0,
|
||||||
"loss": 0.058717112988233566,
|
"loss": 0.043440207839012146,
|
||||||
"image_loss": 0.058717112988233566,
|
"image_loss": 0.043440207839012146,
|
||||||
"smooth_loss": 2.6513908846226286e-09
|
"smooth_loss": 1.4629444322622476e-09
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 3.0,
|
"epoch": 3.0,
|
||||||
"loss": 0.058689698576927185,
|
"loss": 0.04342950880527496,
|
||||||
"image_loss": 0.058689698576927185,
|
"image_loss": 0.04342950880527496,
|
||||||
"smooth_loss": 1.0584165544003099e-08
|
"smooth_loss": 5.5231410556189076e-09
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 4.0,
|
"epoch": 4.0,
|
||||||
"loss": 0.058662042021751404,
|
"loss": 0.04341849684715271,
|
||||||
"image_loss": 0.058662042021751404,
|
"image_loss": 0.04341849684715271,
|
||||||
"smooth_loss": 2.401168686105848e-08
|
"smooth_loss": 1.247603442777745e-08
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 5.0,
|
"epoch": 5.0,
|
||||||
"loss": 0.05863436311483383,
|
"loss": 0.043407391756772995,
|
||||||
"image_loss": 0.05863436311483383,
|
"image_loss": 0.043407391756772995,
|
||||||
"smooth_loss": 4.28750439596115e-08
|
"smooth_loss": 2.2369590624293778e-08
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 6.0,
|
"epoch": 6.0,
|
||||||
"loss": 0.05860644578933716,
|
"loss": 0.04339618235826492,
|
||||||
"image_loss": 0.05860644578933716,
|
"image_loss": 0.04339618235826492,
|
||||||
"smooth_loss": 6.765733928659756e-08
|
"smooth_loss": 3.538238146916228e-08
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 7.0,
|
"epoch": 7.0,
|
||||||
"loss": 0.058578286319971085,
|
"loss": 0.04338495060801506,
|
||||||
"image_loss": 0.058578286319971085,
|
"image_loss": 0.04338495060801506,
|
||||||
"smooth_loss": 9.85552759402708e-08
|
"smooth_loss": 5.16029103891924e-08
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 8.0,
|
"epoch": 8.0,
|
||||||
"loss": 0.05855008214712143,
|
"loss": 0.0433737076818943,
|
||||||
"image_loss": 0.05855008214712143,
|
"image_loss": 0.0433737076818943,
|
||||||
"smooth_loss": 1.3519149888452375e-07
|
"smooth_loss": 7.094314469213714e-08
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 9.0,
|
"epoch": 9.0,
|
||||||
"loss": 0.05852168798446655,
|
"loss": 0.04336243495345116,
|
||||||
"image_loss": 0.05852168798446655,
|
"image_loss": 0.04336243495345116,
|
||||||
"smooth_loss": 1.7822365805386653e-07
|
"smooth_loss": 9.347723306518674e-08
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 10.0,
|
"epoch": 10.0,
|
||||||
"loss": 0.05849294736981392,
|
"loss": 0.04335121437907219,
|
||||||
"image_loss": 0.05849294364452362,
|
"image_loss": 0.04335121437907219,
|
||||||
"smooth_loss": 2.282957041188638e-07
|
"smooth_loss": 1.1908265662441408e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 11.0,
|
"epoch": 11.0,
|
||||||
"loss": 0.058464165776968,
|
"loss": 0.043339941650629044,
|
||||||
"image_loss": 0.058464162051677704,
|
"image_loss": 0.043339941650629044,
|
||||||
"smooth_loss": 2.843589470558072e-07
|
"smooth_loss": 1.482868015045824e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 12.0,
|
"epoch": 12.0,
|
||||||
"loss": 0.05843484401702881,
|
"loss": 0.043328724801540375,
|
||||||
"image_loss": 0.05843484029173851,
|
"image_loss": 0.043328724801540375,
|
||||||
"smooth_loss": 3.4935141002279124e-07
|
"smooth_loss": 1.804429388130302e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 13.0,
|
"epoch": 13.0,
|
||||||
"loss": 0.05840571969747543,
|
"loss": 0.04331750050187111,
|
||||||
"image_loss": 0.058405715972185135,
|
"image_loss": 0.04331749677658081,
|
||||||
"smooth_loss": 4.197248131276865e-07
|
"smooth_loss": 2.166258354918682e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 14.0,
|
"epoch": 14.0,
|
||||||
"loss": 0.05837589502334595,
|
"loss": 0.043306246399879456,
|
||||||
"image_loss": 0.05837589129805565,
|
"image_loss": 0.04330624267458916,
|
||||||
"smooth_loss": 5.000206897420867e-07
|
"smooth_loss": 2.5594988528609974e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 15.0,
|
"epoch": 15.0,
|
||||||
"loss": 0.058346040546894073,
|
"loss": 0.04329502210021019,
|
||||||
"image_loss": 0.05834603309631348,
|
"image_loss": 0.04329501837491989,
|
||||||
"smooth_loss": 5.874838393538084e-07
|
"smooth_loss": 2.9920690280960116e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 16.0,
|
"epoch": 16.0,
|
||||||
"loss": 0.058315880596637726,
|
"loss": 0.043283771723508835,
|
||||||
"image_loss": 0.05831587314605713,
|
"image_loss": 0.043283767998218536,
|
||||||
"smooth_loss": 6.829379231021449e-07
|
"smooth_loss": 3.4602453524712473e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 17.0,
|
"epoch": 17.0,
|
||||||
"loss": 0.05828540772199631,
|
"loss": 0.043272584676742554,
|
||||||
"image_loss": 0.05828540027141571,
|
"image_loss": 0.043272580951452255,
|
||||||
"smooth_loss": 7.876523113736766e-07
|
"smooth_loss": 3.9618711866751255e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 18.0,
|
"epoch": 18.0,
|
||||||
"loss": 0.05825469642877579,
|
"loss": 0.043261270970106125,
|
||||||
"image_loss": 0.05825468897819519,
|
"image_loss": 0.043261267244815826,
|
||||||
"smooth_loss": 9.008151096168149e-07
|
"smooth_loss": 4.4989539560447156e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 19.0,
|
"epoch": 19.0,
|
||||||
"loss": 0.05822316184639931,
|
"loss": 0.04325007647275925,
|
||||||
"image_loss": 0.05822315067052841,
|
"image_loss": 0.04325007274746895,
|
||||||
"smooth_loss": 1.0293301784258801e-06
|
"smooth_loss": 5.079763809590077e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 20.0,
|
"epoch": 20.0,
|
||||||
"loss": 0.05819175764918327,
|
"loss": 0.04323875159025192,
|
||||||
"image_loss": 0.05819174647331238,
|
"image_loss": 0.043238744139671326,
|
||||||
"smooth_loss": 1.1623228601820301e-06
|
"smooth_loss": 5.703672059098608e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 21.0,
|
"epoch": 21.0,
|
||||||
"loss": 0.058159757405519485,
|
"loss": 0.04322751984000206,
|
||||||
"image_loss": 0.05815974250435829,
|
"image_loss": 0.04322751238942146,
|
||||||
"smooth_loss": 1.3093066399960662e-06
|
"smooth_loss": 6.375527732416231e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 22.0,
|
"epoch": 22.0,
|
||||||
"loss": 0.058127511292696,
|
"loss": 0.04321633651852608,
|
||||||
"image_loss": 0.058127496391534805,
|
"image_loss": 0.04321632906794548,
|
||||||
"smooth_loss": 1.465943910261558e-06
|
"smooth_loss": 7.064697911118856e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 23.0,
|
"epoch": 23.0,
|
||||||
"loss": 0.058094657957553864,
|
"loss": 0.043204840272665024,
|
||||||
"image_loss": 0.05809464305639267,
|
"image_loss": 0.04320483282208443,
|
||||||
"smooth_loss": 1.63818526743853e-06
|
"smooth_loss": 7.851771215428016e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 24.0,
|
"epoch": 24.0,
|
||||||
"loss": 0.0580613799393177,
|
"loss": 0.0431935153901577,
|
||||||
"image_loss": 0.05806136131286621,
|
"image_loss": 0.0431935079395771,
|
||||||
"smooth_loss": 1.824913169912179e-06
|
"smooth_loss": 8.643701221444644e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 25.0,
|
"epoch": 25.0,
|
||||||
"loss": 0.05802749842405319,
|
"loss": 0.043182097375392914,
|
||||||
"image_loss": 0.0580274797976017,
|
"image_loss": 0.04318208619952202,
|
||||||
"smooth_loss": 2.0261988993297564e-06
|
"smooth_loss": 9.485592045166413e-07
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 26.0,
|
"epoch": 26.0,
|
||||||
"loss": 0.05799323692917824,
|
"loss": 0.043170712888240814,
|
||||||
"image_loss": 0.05799321457743645,
|
"image_loss": 0.04317070171236992,
|
||||||
"smooth_loss": 2.2423303107643733e-06
|
"smooth_loss": 1.0357232440583175e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 27.0,
|
"epoch": 27.0,
|
||||||
"loss": 0.057958219200372696,
|
"loss": 0.043159086257219315,
|
||||||
"image_loss": 0.05795819312334061,
|
"image_loss": 0.04315907508134842,
|
||||||
"smooth_loss": 2.477491534591536e-06
|
"smooth_loss": 1.1295738886474282e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 28.0,
|
"epoch": 28.0,
|
||||||
"loss": 0.05792278051376343,
|
"loss": 0.043147530406713486,
|
||||||
"image_loss": 0.05792275443673134,
|
"image_loss": 0.04314751923084259,
|
||||||
"smooth_loss": 2.7321416382619645e-06
|
"smooth_loss": 1.2269233593542594e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 29.0,
|
"epoch": 29.0,
|
||||||
"loss": 0.05788649618625641,
|
"loss": 0.04313572868704796,
|
||||||
"image_loss": 0.05788646638393402,
|
"image_loss": 0.043135713785886765,
|
||||||
"smooth_loss": 3.0071300898271147e-06
|
"smooth_loss": 1.3320702691999031e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 30.0,
|
"epoch": 30.0,
|
||||||
"loss": 0.057849571108818054,
|
"loss": 0.04312397539615631,
|
||||||
"image_loss": 0.05784953758120537,
|
"image_loss": 0.04312396049499512,
|
||||||
"smooth_loss": 3.304860001662746e-06
|
"smooth_loss": 1.4407385151571361e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 31.0,
|
"epoch": 31.0,
|
||||||
"loss": 0.05781206861138344,
|
"loss": 0.04311199113726616,
|
||||||
"image_loss": 0.057812031358480453,
|
"image_loss": 0.043111976236104965,
|
||||||
"smooth_loss": 3.6249500681151403e-06
|
"smooth_loss": 1.5546060012638918e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 32.0,
|
"epoch": 32.0,
|
||||||
"loss": 0.05777355656027794,
|
"loss": 0.04309987276792526,
|
||||||
"image_loss": 0.057773515582084656,
|
"image_loss": 0.04309985786676407,
|
||||||
"smooth_loss": 3.9738624764140695e-06
|
"smooth_loss": 1.6753501768107526e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 33.0,
|
"epoch": 33.0,
|
||||||
"loss": 0.057734414935112,
|
"loss": 0.04308755695819855,
|
||||||
"image_loss": 0.05773437023162842,
|
"image_loss": 0.043087538331747055,
|
||||||
"smooth_loss": 4.350997187430039e-06
|
"smooth_loss": 1.8031345234703622e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 34.0,
|
"epoch": 34.0,
|
||||||
"loss": 0.057694148272275925,
|
"loss": 0.04307503253221512,
|
||||||
"image_loss": 0.057694099843502045,
|
"image_loss": 0.043075013905763626,
|
||||||
"smooth_loss": 4.76802779303398e-06
|
"smooth_loss": 1.9371573216631077e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 35.0,
|
"epoch": 35.0,
|
||||||
"loss": 0.05765282362699509,
|
"loss": 0.0430622436106205,
|
||||||
"image_loss": 0.05765277147293091,
|
"image_loss": 0.04306222125887871,
|
||||||
"smooth_loss": 5.221423634793609e-06
|
"smooth_loss": 2.0795719137822744e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 36.0,
|
"epoch": 36.0,
|
||||||
"loss": 0.05761110037565231,
|
"loss": 0.04304904118180275,
|
||||||
"image_loss": 0.057611044496297836,
|
"image_loss": 0.04304901883006096,
|
||||||
"smooth_loss": 5.697434062312823e-06
|
"smooth_loss": 2.2306476239464246e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 37.0,
|
"epoch": 37.0,
|
||||||
"loss": 0.05756793171167374,
|
"loss": 0.04303569346666336,
|
||||||
"image_loss": 0.05756786838173866,
|
"image_loss": 0.04303567111492157,
|
||||||
"smooth_loss": 6.224857315828558e-06
|
"smooth_loss": 2.3862683065090096e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 38.0,
|
"epoch": 38.0,
|
||||||
"loss": 0.05752401426434517,
|
"loss": 0.043021950870752335,
|
||||||
"image_loss": 0.0575239472091198,
|
"image_loss": 0.043021924793720245,
|
||||||
"smooth_loss": 6.778077477065381e-06
|
"smooth_loss": 2.5546942197252065e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 39.0,
|
"epoch": 39.0,
|
||||||
"loss": 0.05747854709625244,
|
"loss": 0.04300765320658684,
|
||||||
"image_loss": 0.05747847259044647,
|
"image_loss": 0.04300762712955475,
|
||||||
"smooth_loss": 7.391299732262269e-06
|
"smooth_loss": 2.7329397198627703e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 40.0,
|
"epoch": 40.0,
|
||||||
"loss": 0.057432375848293304,
|
"loss": 0.04299306869506836,
|
||||||
"image_loss": 0.05743229389190674,
|
"image_loss": 0.04299303889274597,
|
||||||
"smooth_loss": 8.039884960453492e-06
|
"smooth_loss": 2.9173079383326694e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 41.0,
|
"epoch": 41.0,
|
||||||
"loss": 0.0573841817677021,
|
"loss": 0.04297754168510437,
|
||||||
"image_loss": 0.05738409236073494,
|
"image_loss": 0.04297751188278198,
|
||||||
"smooth_loss": 8.764058293309063e-06
|
"smooth_loss": 3.1240251701092348e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 42.0,
|
"epoch": 42.0,
|
||||||
"loss": 0.057334933429956436,
|
"loss": 0.042961329221725464,
|
||||||
"image_loss": 0.057334836572408676,
|
"image_loss": 0.04296129569411278,
|
||||||
"smooth_loss": 9.537392543279566e-06
|
"smooth_loss": 3.3366113711963408e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 43.0,
|
"epoch": 43.0,
|
||||||
"loss": 0.057283785194158554,
|
"loss": 0.04294413700699806,
|
||||||
"image_loss": 0.0572836808860302,
|
"image_loss": 0.04294409975409508,
|
||||||
"smooth_loss": 1.0389785529696383e-05
|
"smooth_loss": 3.5652053611556767e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 44.0,
|
"epoch": 44.0,
|
||||||
"loss": 0.05723174661397934,
|
"loss": 0.04292583465576172,
|
||||||
"image_loss": 0.057231634855270386,
|
"image_loss": 0.042925797402858734,
|
||||||
"smooth_loss": 1.1303329301881604e-05
|
"smooth_loss": 3.8064451928221388e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 45.0,
|
"epoch": 45.0,
|
||||||
"loss": 0.057176779955625534,
|
"loss": 0.04290642961859703,
|
||||||
"image_loss": 0.057176657021045685,
|
"image_loss": 0.04290638864040375,
|
||||||
"smooth_loss": 1.2341002729954198e-05
|
"smooth_loss": 4.066736892127665e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 46.0,
|
"epoch": 46.0,
|
||||||
"loss": 0.057120226323604584,
|
"loss": 0.04288554936647415,
|
||||||
"image_loss": 0.05712009221315384,
|
"image_loss": 0.04288550466299057,
|
||||||
"smooth_loss": 1.3473189028445631e-05
|
"smooth_loss": 4.346161858848063e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 47.0,
|
"epoch": 47.0,
|
||||||
"loss": 0.057062555104494095,
|
"loss": 0.04286346957087517,
|
||||||
"image_loss": 0.057062409818172455,
|
"image_loss": 0.042863424867391586,
|
||||||
"smooth_loss": 1.4687300790683366e-05
|
"smooth_loss": 4.647703008231474e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 48.0,
|
"epoch": 48.0,
|
||||||
"loss": 0.05700160190463066,
|
"loss": 0.042839810252189636,
|
||||||
"image_loss": 0.05700144171714783,
|
"image_loss": 0.042839761823415756,
|
||||||
"smooth_loss": 1.6045180018409155e-05
|
"smooth_loss": 4.978734978067223e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 49.0,
|
"epoch": 49.0,
|
||||||
"loss": 0.056939497590065,
|
"loss": 0.04281448572874069,
|
||||||
"image_loss": 0.056939322501420975,
|
"image_loss": 0.042814433574676514,
|
||||||
"smooth_loss": 1.7514281353214756e-05
|
"smooth_loss": 5.339582457963843e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 50.0,
|
"epoch": 50.0,
|
||||||
"loss": 0.05687557905912399,
|
"loss": 0.04278765991330147,
|
||||||
"image_loss": 0.05687538906931877,
|
"image_loss": 0.04278760403394699,
|
||||||
"smooth_loss": 1.911029903567396e-05
|
"smooth_loss": 5.742196663049981e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 51.0,
|
"epoch": 51.0,
|
||||||
"loss": 0.056809235364198685,
|
"loss": 0.04275878146290779,
|
||||||
"image_loss": 0.05680902674794197,
|
"image_loss": 0.04275871813297272,
|
||||||
"smooth_loss": 2.0875198970315978e-05
|
"smooth_loss": 6.183304776641307e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 52.0,
|
"epoch": 52.0,
|
||||||
"loss": 0.05674029514193535,
|
"loss": 0.04272802174091339,
|
||||||
"image_loss": 0.05674006789922714,
|
"image_loss": 0.04272795468568802,
|
||||||
"smooth_loss": 2.2824997358839028e-05
|
"smooth_loss": 6.67552012600936e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 53.0,
|
"epoch": 53.0,
|
||||||
"loss": 0.056669920682907104,
|
"loss": 0.042694948613643646,
|
||||||
"image_loss": 0.05666967108845711,
|
"image_loss": 0.042694877833127975,
|
||||||
"smooth_loss": 2.4954817490652204e-05
|
"smooth_loss": 7.233465112221893e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 54.0,
|
"epoch": 54.0,
|
||||||
"loss": 0.0565965436398983,
|
"loss": 0.04265972599387169,
|
||||||
"image_loss": 0.05659627169370651,
|
"image_loss": 0.04265964776277542,
|
||||||
"smooth_loss": 2.7323461836203933e-05
|
"smooth_loss": 7.846486369089689e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 55.0,
|
"epoch": 55.0,
|
||||||
"loss": 0.056521233171224594,
|
"loss": 0.04262172058224678,
|
||||||
"image_loss": 0.05652093514800072,
|
"image_loss": 0.042621634900569916,
|
||||||
"smooth_loss": 2.9880573492846452e-05
|
"smooth_loss": 8.557326509617269e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 56.0,
|
"epoch": 56.0,
|
||||||
"loss": 0.05644303560256958,
|
"loss": 0.04258149117231369,
|
||||||
"image_loss": 0.056442707777023315,
|
"image_loss": 0.04258139804005623,
|
||||||
"smooth_loss": 3.275117705925368e-05
|
"smooth_loss": 9.357276212540455e-06
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 57.0,
|
"epoch": 57.0,
|
||||||
"loss": 0.05636239051818848,
|
"loss": 0.042538225650787354,
|
||||||
"image_loss": 0.056362032890319824,
|
"image_loss": 0.042538121342659,
|
||||||
"smooth_loss": 3.594645386328921e-05
|
"smooth_loss": 1.0288686098647304e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 58.0,
|
"epoch": 58.0,
|
||||||
"loss": 0.056279003620147705,
|
"loss": 0.042492616921663284,
|
||||||
"image_loss": 0.05627860873937607,
|
"image_loss": 0.04249250143766403,
|
||||||
"smooth_loss": 3.945654316339642e-05
|
"smooth_loss": 1.136589708039537e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 59.0,
|
"epoch": 59.0,
|
||||||
"loss": 0.05619405582547188,
|
"loss": 0.042444635182619095,
|
||||||
"image_loss": 0.056193623691797256,
|
"image_loss": 0.04244450852274895,
|
||||||
"smooth_loss": 4.33080131188035e-05
|
"smooth_loss": 1.261604847968556e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 60.0,
|
"epoch": 60.0,
|
||||||
"loss": 0.056105729192495346,
|
"loss": 0.042394258081912994,
|
||||||
"image_loss": 0.05610525235533714,
|
"image_loss": 0.04239411652088165,
|
||||||
"smooth_loss": 4.769213410327211e-05
|
"smooth_loss": 1.4080704204388894e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 61.0,
|
"epoch": 61.0,
|
||||||
"loss": 0.0560150220990181,
|
"loss": 0.04234214872121811,
|
||||||
"image_loss": 0.056014496833086014,
|
"image_loss": 0.042341988533735275,
|
||||||
"smooth_loss": 5.2607003453886136e-05
|
"smooth_loss": 1.583471203048248e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 62.0,
|
"epoch": 62.0,
|
||||||
"loss": 0.05592316761612892,
|
"loss": 0.042289428412914276,
|
||||||
"image_loss": 0.05592258647084236,
|
"image_loss": 0.04228924959897995,
|
||||||
"smooth_loss": 5.80552987230476e-05
|
"smooth_loss": 1.7939397366717458e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 63.0,
|
"epoch": 63.0,
|
||||||
"loss": 0.05583030730485916,
|
"loss": 0.04223756119608879,
|
||||||
"image_loss": 0.055829666554927826,
|
"image_loss": 0.042237356305122375,
|
||||||
"smooth_loss": 6.423084414564073e-05
|
"smooth_loss": 2.041880725300871e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 64.0,
|
"epoch": 64.0,
|
||||||
"loss": 0.055736664682626724,
|
"loss": 0.04218873754143715,
|
||||||
"image_loss": 0.05573595315217972,
|
"image_loss": 0.042188502848148346,
|
||||||
"smooth_loss": 7.123746036086231e-05
|
"smooth_loss": 2.3403612431138754e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 65.0,
|
"epoch": 65.0,
|
||||||
"loss": 0.0556456595659256,
|
"loss": 0.04214523360133171,
|
||||||
"image_loss": 0.055644869804382324,
|
"image_loss": 0.04214496165513992,
|
||||||
"smooth_loss": 7.909350097179413e-05
|
"smooth_loss": 2.7044574380852282e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 66.0,
|
"epoch": 66.0,
|
||||||
"loss": 0.05555867403745651,
|
"loss": 0.042111899703741074,
|
||||||
"image_loss": 0.055557794868946075,
|
"image_loss": 0.042111586779356,
|
||||||
"smooth_loss": 8.807046106085181e-05
|
"smooth_loss": 3.138223473797552e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 67.0,
|
"epoch": 67.0,
|
||||||
"loss": 0.055479418486356735,
|
"loss": 0.04209335148334503,
|
||||||
"image_loss": 0.05547843500971794,
|
"image_loss": 0.04209298640489578,
|
||||||
"smooth_loss": 9.828618203755468e-05
|
"smooth_loss": 3.650638245744631e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 68.0,
|
"epoch": 68.0,
|
||||||
"loss": 0.05541320890188217,
|
"loss": 0.042094651609659195,
|
||||||
"image_loss": 0.055412109941244125,
|
"image_loss": 0.04209422692656517,
|
||||||
"smooth_loss": 0.00010981244122376665
|
"smooth_loss": 4.229835394653492e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 69.0,
|
"epoch": 69.0,
|
||||||
"loss": 0.05536733567714691,
|
"loss": 0.0421144962310791,
|
||||||
"image_loss": 0.05536610633134842,
|
"image_loss": 0.0421140156686306,
|
||||||
"smooth_loss": 0.00012293846521060914
|
"smooth_loss": 4.809771417058073e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 70.0,
|
"epoch": 70.0,
|
||||||
"loss": 0.05535025894641876,
|
"loss": 0.04213815554976463,
|
||||||
"image_loss": 0.05534888431429863,
|
"image_loss": 0.04213763028383255,
|
||||||
"smooth_loss": 0.00013746831973548979
|
"smooth_loss": 5.256465738057159e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 71.0,
|
"epoch": 71.0,
|
||||||
"loss": 0.0553678423166275,
|
"loss": 0.04214761406183243,
|
||||||
"image_loss": 0.05536631867289543,
|
"image_loss": 0.042147066444158554,
|
||||||
"smooth_loss": 0.00015228186384774745
|
"smooth_loss": 5.478101593325846e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 72.0,
|
"epoch": 72.0,
|
||||||
"loss": 0.05540918558835983,
|
"loss": 0.04213856905698776,
|
||||||
"image_loss": 0.055407531559467316,
|
"image_loss": 0.04213802143931389,
|
||||||
"smooth_loss": 0.00016536397743038833
|
"smooth_loss": 5.4818134231027216e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 73.0,
|
"epoch": 73.0,
|
||||||
"loss": 0.05544228106737137,
|
"loss": 0.04211747646331787,
|
||||||
"image_loss": 0.05544053763151169,
|
"image_loss": 0.04211694374680519,
|
||||||
"smooth_loss": 0.00017419658252038062
|
"smooth_loss": 5.3270672651706263e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 74.0,
|
"epoch": 74.0,
|
||||||
"loss": 0.05544540658593178,
|
"loss": 0.042093425989151,
|
||||||
"image_loss": 0.05544362962245941,
|
"image_loss": 0.04209291934967041,
|
||||||
"smooth_loss": 0.00017784371448215097
|
"smooth_loss": 5.081955896457657e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 75.0,
|
"epoch": 75.0,
|
||||||
"loss": 0.05542201176285744,
|
"loss": 0.04207289591431618,
|
||||||
"image_loss": 0.05542023852467537,
|
"image_loss": 0.042072415351867676,
|
||||||
"smooth_loss": 0.00017743516946211457
|
"smooth_loss": 4.802431067219004e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 76.0,
|
"epoch": 76.0,
|
||||||
"loss": 0.0553855374455452,
|
"loss": 0.04205827787518501,
|
||||||
"image_loss": 0.055383794009685516,
|
"image_loss": 0.0420578271150589,
|
||||||
"smooth_loss": 0.0001741933374432847
|
"smooth_loss": 4.526200791588053e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 77.0,
|
"epoch": 77.0,
|
||||||
"loss": 0.05534776300191879,
|
"loss": 0.042049601674079895,
|
||||||
"image_loss": 0.05534606799483299,
|
"image_loss": 0.04204917326569557,
|
||||||
"smooth_loss": 0.00016937614418566227
|
"smooth_loss": 4.2754021706059575e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 78.0,
|
"epoch": 78.0,
|
||||||
"loss": 0.05531612038612366,
|
"loss": 0.042045459151268005,
|
||||||
"image_loss": 0.055314477533102036,
|
"image_loss": 0.04204505309462547,
|
||||||
"smooth_loss": 0.00016410183161497116
|
"smooth_loss": 4.061676736455411e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 79.0,
|
"epoch": 79.0,
|
||||||
"loss": 0.05529369041323662,
|
"loss": 0.04204433411359787,
|
||||||
"image_loss": 0.055292099714279175,
|
"image_loss": 0.04204394668340683,
|
||||||
"smooth_loss": 0.00015910626098047942
|
"smooth_loss": 3.89082488254644e-05
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"epoch": 80.0,
|
"epoch": 80.0,
|
||||||
"loss": 0.055279314517974854,
|
"loss": 0.04204464331269264,
|
||||||
"image_loss": 0.05527777224779129,
|
"image_loss": 0.0420442670583725,
|
||||||
"smooth_loss": 0.00015433150110766292
|
"smooth_loss": 3.758826642297208e-05
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
Binary file not shown.
57
outputs/preprocessed/patient1_neck_alignment.json
Normal file
57
outputs/preprocessed/patient1_neck_alignment.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"fixed_input_path": "outputs/nifti/patient1_fixed.nii.gz",
|
||||||
|
"moving_input_path": "outputs/nifti/patient1_moving.nii.gz",
|
||||||
|
"fixed_output_path": "outputs/preprocessed/patient1_fixed_preprocessed.nii.gz",
|
||||||
|
"moving_output_path": "outputs/preprocessed/patient1_moving_preprocessed.nii.gz",
|
||||||
|
"fixed_resampled_shape_xyz": [
|
||||||
|
288,
|
||||||
|
288,
|
||||||
|
317
|
||||||
|
],
|
||||||
|
"moving_resampled_shape_xyz": [
|
||||||
|
331,
|
||||||
|
331,
|
||||||
|
334
|
||||||
|
],
|
||||||
|
"target_shape_xyz": [
|
||||||
|
320,
|
||||||
|
320,
|
||||||
|
352
|
||||||
|
],
|
||||||
|
"target_spacing_xyz": [
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"fixed_neck_center_index_xyz": [
|
||||||
|
143.0,
|
||||||
|
160.5,
|
||||||
|
159.5
|
||||||
|
],
|
||||||
|
"moving_neck_center_index_xyz": [
|
||||||
|
164.5,
|
||||||
|
195.0,
|
||||||
|
170.0
|
||||||
|
],
|
||||||
|
"fixed_neck_center_world_mm": [
|
||||||
|
13.233566284179688,
|
||||||
|
-33.013214111328125,
|
||||||
|
-633.6527099609375
|
||||||
|
],
|
||||||
|
"moving_neck_center_world_mm": [
|
||||||
|
13.2132568359375,
|
||||||
|
18.1754150390625,
|
||||||
|
-656.634033203125
|
||||||
|
],
|
||||||
|
"moving_translation_mm": [
|
||||||
|
0.0203094482421875,
|
||||||
|
-51.188629150390625,
|
||||||
|
22.9813232421875
|
||||||
|
],
|
||||||
|
"body_threshold_hu": -500.0,
|
||||||
|
"foreground_percentiles": [
|
||||||
|
2.0,
|
||||||
|
98.0
|
||||||
|
],
|
||||||
|
"method": "neck-mask-centroid-translation"
|
||||||
|
}
|
||||||
297
prealign.py
Normal file
297
prealign.py
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
"""颈部基准的 CT 预配准。
|
||||||
|
|
||||||
|
这一阶段发生在 VoxelMorph 训练/推理之前:
|
||||||
|
1. Fixed 与 Moving 原始 NIfTI 先重采样到同一 spacing。
|
||||||
|
2. 基于 CT foreground mask 估计颈部/身体区域的稳健中心。
|
||||||
|
3. 将 Moving 平移到 Fixed 的颈部中心。
|
||||||
|
4. 在同一个物理网格中裁剪/填充并归一化,得到后续 VoxelMorph 输入。
|
||||||
|
|
||||||
|
当前实现是确定性的平移预配准,目标是先解决层数不同、扫描范围不同导致的
|
||||||
|
初始空间不一致;细致非线性变形仍交给官方 VoxelMorph。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable, Sequence, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from scipy import ndimage
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
DEFAULT_FIXED_NIFTI,
|
||||||
|
DEFAULT_MOVING_NIFTI,
|
||||||
|
DEFAULT_TARGET_SHAPE,
|
||||||
|
DEFAULT_TARGET_SPACING,
|
||||||
|
DEFAULT_WINDOW_LEVEL,
|
||||||
|
DEFAULT_WINDOW_WIDTH,
|
||||||
|
PREPROCESSED_DIR,
|
||||||
|
)
|
||||||
|
from preprocess import (
|
||||||
|
affine_with_spacing,
|
||||||
|
ensure_multiple_of_16,
|
||||||
|
load_nifti,
|
||||||
|
resample_to_spacing,
|
||||||
|
save_nifti,
|
||||||
|
window_and_normalize,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NeckAlignmentMeta:
|
||||||
|
fixed_input_path: str
|
||||||
|
moving_input_path: str
|
||||||
|
fixed_output_path: str
|
||||||
|
moving_output_path: str
|
||||||
|
fixed_resampled_shape_xyz: Tuple[int, int, int]
|
||||||
|
moving_resampled_shape_xyz: Tuple[int, int, int]
|
||||||
|
target_shape_xyz: Tuple[int, int, int]
|
||||||
|
target_spacing_xyz: Tuple[float, float, float]
|
||||||
|
fixed_neck_center_index_xyz: Tuple[float, float, float]
|
||||||
|
moving_neck_center_index_xyz: Tuple[float, float, float]
|
||||||
|
fixed_neck_center_world_mm: Tuple[float, float, float]
|
||||||
|
moving_neck_center_world_mm: Tuple[float, float, float]
|
||||||
|
moving_translation_mm: Tuple[float, float, float]
|
||||||
|
body_threshold_hu: float
|
||||||
|
foreground_percentiles: Tuple[float, float]
|
||||||
|
method: str = "neck-mask-centroid-translation"
|
||||||
|
|
||||||
|
|
||||||
|
def _tuple_float(values: Sequence[float]) -> Tuple[float, float, float]:
|
||||||
|
return tuple(float(v) for v in values) # type: ignore[return-value]
|
||||||
|
|
||||||
|
|
||||||
|
def _tuple_int(values: Sequence[int]) -> Tuple[int, int, int]:
|
||||||
|
return tuple(int(v) for v in values) # type: ignore[return-value]
|
||||||
|
|
||||||
|
|
||||||
|
def index_to_world(affine: np.ndarray, index_xyz: Sequence[float]) -> np.ndarray:
|
||||||
|
index = np.asarray(index_xyz, dtype=np.float64)
|
||||||
|
return affine[:3, :3] @ index + affine[:3, 3]
|
||||||
|
|
||||||
|
|
||||||
|
def largest_foreground_component(mask: np.ndarray) -> np.ndarray:
|
||||||
|
structure = ndimage.generate_binary_structure(rank=3, connectivity=1)
|
||||||
|
labeled, count = ndimage.label(mask, structure=structure)
|
||||||
|
if count < 1:
|
||||||
|
raise ValueError("未能从 CT 中提取颈部 foreground mask。")
|
||||||
|
sizes = np.bincount(labeled.ravel())
|
||||||
|
sizes[0] = 0
|
||||||
|
return labeled == int(np.argmax(sizes))
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_neck_center_index(
|
||||||
|
data_hu: np.ndarray,
|
||||||
|
body_threshold_hu: float = -500.0,
|
||||||
|
foreground_percentiles: Sequence[float] = (2.0, 98.0),
|
||||||
|
) -> Tuple[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
|
||||||
|
"""估计颈部 foreground 的稳健中心。
|
||||||
|
|
||||||
|
先用 HU 阈值去掉空气,再保留最大连通域,最后用坐标百分位框的中心而不是
|
||||||
|
简单均值,减少局部噪声、少量外部物体和扫描边界的影响。
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = np.asarray(data_hu, dtype=np.float32)
|
||||||
|
mask = np.isfinite(data) & (data > float(body_threshold_hu)) & (data < 3000.0)
|
||||||
|
mask = largest_foreground_component(mask)
|
||||||
|
coords = np.where(mask)
|
||||||
|
if not coords or coords[0].size == 0:
|
||||||
|
raise ValueError("颈部 foreground mask 为空。")
|
||||||
|
|
||||||
|
p_low, p_high = (float(v) for v in foreground_percentiles)
|
||||||
|
lows = np.asarray([np.percentile(axis_coords, p_low) for axis_coords in coords], dtype=np.float64)
|
||||||
|
highs = np.asarray([np.percentile(axis_coords, p_high) for axis_coords in coords], dtype=np.float64)
|
||||||
|
center = (lows + highs) / 2.0
|
||||||
|
return center, (lows, highs)
|
||||||
|
|
||||||
|
|
||||||
|
def target_affine_from_center(
|
||||||
|
reference_affine: np.ndarray,
|
||||||
|
center_world_mm: Sequence[float],
|
||||||
|
target_shape: Sequence[int],
|
||||||
|
target_spacing: Sequence[float],
|
||||||
|
) -> np.ndarray:
|
||||||
|
output_affine = affine_with_spacing(reference_affine, target_spacing)
|
||||||
|
center_index = (np.asarray(target_shape, dtype=np.float64) - 1.0) / 2.0
|
||||||
|
center_world = np.asarray(center_world_mm, dtype=np.float64)
|
||||||
|
output_affine[:3, 3] = center_world - output_affine[:3, :3] @ center_index
|
||||||
|
return output_affine
|
||||||
|
|
||||||
|
|
||||||
|
def resample_to_reference_grid(
|
||||||
|
data: np.ndarray,
|
||||||
|
input_affine: np.ndarray,
|
||||||
|
output_affine: np.ndarray,
|
||||||
|
output_shape: Sequence[int],
|
||||||
|
order: int = 1,
|
||||||
|
cval: float = -1000.0,
|
||||||
|
) -> np.ndarray:
|
||||||
|
input_to_output = np.linalg.inv(input_affine) @ output_affine
|
||||||
|
matrix = input_to_output[:3, :3]
|
||||||
|
offset = input_to_output[:3, 3]
|
||||||
|
output = ndimage.affine_transform(
|
||||||
|
data.astype(np.float32, copy=False),
|
||||||
|
matrix=matrix,
|
||||||
|
offset=offset,
|
||||||
|
output_shape=tuple(int(v) for v in output_shape),
|
||||||
|
order=order,
|
||||||
|
mode="constant",
|
||||||
|
cval=float(cval),
|
||||||
|
prefilter=(order > 1),
|
||||||
|
output=np.float32,
|
||||||
|
)
|
||||||
|
return output.astype(np.float32, copy=False)
|
||||||
|
|
||||||
|
|
||||||
|
def prealign_pair(
|
||||||
|
fixed_input_path: str | Path,
|
||||||
|
moving_input_path: str | Path,
|
||||||
|
fixed_output_path: str | Path = DEFAULT_FIXED_NIFTI,
|
||||||
|
moving_output_path: str | Path = DEFAULT_MOVING_NIFTI,
|
||||||
|
target_spacing: Sequence[float] = DEFAULT_TARGET_SPACING,
|
||||||
|
target_shape: Sequence[int] = DEFAULT_TARGET_SHAPE,
|
||||||
|
window_width: float = DEFAULT_WINDOW_WIDTH,
|
||||||
|
window_level: float = DEFAULT_WINDOW_LEVEL,
|
||||||
|
body_threshold_hu: float = -500.0,
|
||||||
|
foreground_percentiles: Sequence[float] = (2.0, 98.0),
|
||||||
|
max_memory_mb: int = 8192,
|
||||||
|
metadata_json: str | Path | None = None,
|
||||||
|
) -> NeckAlignmentMeta:
|
||||||
|
target_shape = ensure_multiple_of_16(target_shape)
|
||||||
|
fixed_input_path = Path(fixed_input_path)
|
||||||
|
moving_input_path = Path(moving_input_path)
|
||||||
|
fixed_output_path = Path(fixed_output_path)
|
||||||
|
moving_output_path = Path(moving_output_path)
|
||||||
|
|
||||||
|
fixed_hu, fixed_affine, fixed_spacing = load_nifti(fixed_input_path)
|
||||||
|
moving_hu, moving_affine, moving_spacing = load_nifti(moving_input_path)
|
||||||
|
|
||||||
|
fixed_resampled, fixed_resampled_affine = resample_to_spacing(
|
||||||
|
fixed_hu,
|
||||||
|
fixed_affine,
|
||||||
|
current_spacing=fixed_spacing,
|
||||||
|
target_spacing=target_spacing,
|
||||||
|
order=1,
|
||||||
|
max_memory_mb=max_memory_mb,
|
||||||
|
)
|
||||||
|
moving_resampled, moving_resampled_affine = resample_to_spacing(
|
||||||
|
moving_hu,
|
||||||
|
moving_affine,
|
||||||
|
current_spacing=moving_spacing,
|
||||||
|
target_spacing=target_spacing,
|
||||||
|
order=1,
|
||||||
|
max_memory_mb=max_memory_mb,
|
||||||
|
)
|
||||||
|
|
||||||
|
fixed_center_idx, _ = estimate_neck_center_index(
|
||||||
|
fixed_resampled,
|
||||||
|
body_threshold_hu=body_threshold_hu,
|
||||||
|
foreground_percentiles=foreground_percentiles,
|
||||||
|
)
|
||||||
|
moving_center_idx, _ = estimate_neck_center_index(
|
||||||
|
moving_resampled,
|
||||||
|
body_threshold_hu=body_threshold_hu,
|
||||||
|
foreground_percentiles=foreground_percentiles,
|
||||||
|
)
|
||||||
|
fixed_center_world = index_to_world(fixed_resampled_affine, fixed_center_idx)
|
||||||
|
moving_center_world = index_to_world(moving_resampled_affine, moving_center_idx)
|
||||||
|
moving_translation = fixed_center_world - moving_center_world
|
||||||
|
|
||||||
|
moving_aligned_affine = moving_resampled_affine.copy()
|
||||||
|
moving_aligned_affine[:3, 3] = moving_aligned_affine[:3, 3] + moving_translation
|
||||||
|
output_affine = target_affine_from_center(
|
||||||
|
fixed_resampled_affine,
|
||||||
|
center_world_mm=fixed_center_world,
|
||||||
|
target_shape=target_shape,
|
||||||
|
target_spacing=target_spacing,
|
||||||
|
)
|
||||||
|
|
||||||
|
fixed_grid_hu = resample_to_reference_grid(
|
||||||
|
fixed_resampled,
|
||||||
|
input_affine=fixed_resampled_affine,
|
||||||
|
output_affine=output_affine,
|
||||||
|
output_shape=target_shape,
|
||||||
|
order=1,
|
||||||
|
cval=-1000.0,
|
||||||
|
)
|
||||||
|
moving_grid_hu = resample_to_reference_grid(
|
||||||
|
moving_resampled,
|
||||||
|
input_affine=moving_aligned_affine,
|
||||||
|
output_affine=output_affine,
|
||||||
|
output_shape=target_shape,
|
||||||
|
order=1,
|
||||||
|
cval=-1000.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
fixed_norm = window_and_normalize(fixed_grid_hu, window_width=window_width, window_level=window_level)
|
||||||
|
moving_norm = window_and_normalize(moving_grid_hu, window_width=window_width, window_level=window_level)
|
||||||
|
save_nifti(fixed_norm, output_affine, fixed_output_path)
|
||||||
|
save_nifti(moving_norm, output_affine, moving_output_path)
|
||||||
|
|
||||||
|
meta = NeckAlignmentMeta(
|
||||||
|
fixed_input_path=str(fixed_input_path),
|
||||||
|
moving_input_path=str(moving_input_path),
|
||||||
|
fixed_output_path=str(fixed_output_path),
|
||||||
|
moving_output_path=str(moving_output_path),
|
||||||
|
fixed_resampled_shape_xyz=_tuple_int(fixed_resampled.shape),
|
||||||
|
moving_resampled_shape_xyz=_tuple_int(moving_resampled.shape),
|
||||||
|
target_shape_xyz=_tuple_int(target_shape),
|
||||||
|
target_spacing_xyz=_tuple_float(target_spacing),
|
||||||
|
fixed_neck_center_index_xyz=_tuple_float(fixed_center_idx),
|
||||||
|
moving_neck_center_index_xyz=_tuple_float(moving_center_idx),
|
||||||
|
fixed_neck_center_world_mm=_tuple_float(fixed_center_world),
|
||||||
|
moving_neck_center_world_mm=_tuple_float(moving_center_world),
|
||||||
|
moving_translation_mm=_tuple_float(moving_translation),
|
||||||
|
body_threshold_hu=float(body_threshold_hu),
|
||||||
|
foreground_percentiles=(float(foreground_percentiles[0]), float(foreground_percentiles[1])),
|
||||||
|
)
|
||||||
|
|
||||||
|
if metadata_json is None:
|
||||||
|
metadata_json = PREPROCESSED_DIR / "patient1_neck_alignment.json"
|
||||||
|
metadata_path = Path(metadata_json)
|
||||||
|
metadata_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
metadata_path.write_text(json.dumps(asdict(meta), ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
|
def build_arg_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description="以颈部 foreground 为基准的 CT 平移预配准。")
|
||||||
|
parser.add_argument("--fixed", required=True, help="Fixed 原始 NIfTI。")
|
||||||
|
parser.add_argument("--moving", required=True, help="Moving 原始 NIfTI。")
|
||||||
|
parser.add_argument("--fixed-output", default=str(DEFAULT_FIXED_NIFTI))
|
||||||
|
parser.add_argument("--moving-output", default=str(DEFAULT_MOVING_NIFTI))
|
||||||
|
parser.add_argument("--target-spacing", type=float, nargs=3, default=DEFAULT_TARGET_SPACING, metavar=("X", "Y", "Z"))
|
||||||
|
parser.add_argument("--target-shape", type=int, nargs=3, default=DEFAULT_TARGET_SHAPE, metavar=("X", "Y", "Z"))
|
||||||
|
parser.add_argument("--window-width", type=float, default=DEFAULT_WINDOW_WIDTH)
|
||||||
|
parser.add_argument("--window-level", type=float, default=DEFAULT_WINDOW_LEVEL)
|
||||||
|
parser.add_argument("--body-threshold-hu", type=float, default=-500.0)
|
||||||
|
parser.add_argument("--foreground-percentiles", type=float, nargs=2, default=(2.0, 98.0))
|
||||||
|
parser.add_argument("--metadata-json", default=str(PREPROCESSED_DIR / "patient1_neck_alignment.json"))
|
||||||
|
parser.add_argument("--max-memory-mb", type=int, default=8192)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Iterable[str] | None = None) -> None:
|
||||||
|
args = build_arg_parser().parse_args(argv)
|
||||||
|
meta = prealign_pair(
|
||||||
|
fixed_input_path=args.fixed,
|
||||||
|
moving_input_path=args.moving,
|
||||||
|
fixed_output_path=args.fixed_output,
|
||||||
|
moving_output_path=args.moving_output,
|
||||||
|
target_spacing=args.target_spacing,
|
||||||
|
target_shape=args.target_shape,
|
||||||
|
window_width=args.window_width,
|
||||||
|
window_level=args.window_level,
|
||||||
|
body_threshold_hu=args.body_threshold_hu,
|
||||||
|
foreground_percentiles=args.foreground_percentiles,
|
||||||
|
metadata_json=args.metadata_json,
|
||||||
|
max_memory_mb=args.max_memory_mb,
|
||||||
|
)
|
||||||
|
print(json.dumps(asdict(meta), ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -53,9 +53,9 @@ def ensure_multiple_of_16(shape: Sequence[int]) -> Tuple[int, int, int]:
|
|||||||
shape = tuple(int(v) for v in shape)
|
shape = tuple(int(v) for v in shape)
|
||||||
if len(shape) != 3:
|
if len(shape) != 3:
|
||||||
raise ValueError("target_shape 必须包含 3 个维度。")
|
raise ValueError("target_shape 必须包含 3 个维度。")
|
||||||
bad = [v for v in shape if v <= 0 or v % 16 != 0]
|
bad = [v for v in shape if v <= 0 or v % 32 != 0]
|
||||||
if bad:
|
if bad:
|
||||||
raise ValueError(f"VoxelMorph 建议三维尺寸均为 16 的倍数,当前非法维度: {bad}")
|
raise ValueError(f"VoxelMorph 当前 U-Net 建议三维尺寸均为 32 的倍数,当前非法维度: {bad}")
|
||||||
return shape # type: ignore[return-value]
|
return shape # type: ignore[return-value]
|
||||||
|
|
||||||
|
|
||||||
@@ -324,4 +324,3 @@ def main(argv: Iterable[str] | None = None) -> None:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user