2026-05-03-22-02-15 调整预览分界线绘制

This commit is contained in:
2026-05-03 22:09:19 +08:00
parent e431830d15
commit 525c2c1dda
6 changed files with 299 additions and 18 deletions

View File

@@ -123,14 +123,21 @@ def cutoff_center_z(coordinates_cutoff):
return float(np.mean([point[0] for point in coordinates_cutoff]))
def draw_cutoff_line(panel, image_depth, coordinates_cutoff=DEFAULT_COORDINATES_CUTOFF):
panel = panel.copy()
def cutoff_line_y(panel, image_depth, coordinates_cutoff=DEFAULT_COORDINATES_CUTOFF):
crop_height = int(image_depth * 0.72)
if crop_height <= 0:
return panel
return None
line_y = int(round((image_depth - 1 - cutoff_center_z(coordinates_cutoff)) * panel.height / crop_height))
if line_y < 0 or line_y >= panel.height:
return None
return line_y
def draw_cutoff_line(panel, image_depth, coordinates_cutoff=DEFAULT_COORDINATES_CUTOFF):
panel = panel.copy()
line_y = cutoff_line_y(panel, image_depth, coordinates_cutoff)
if line_y is None:
return panel
draw = ImageDraw.Draw(panel)
@@ -141,6 +148,57 @@ def draw_cutoff_line(panel, image_depth, coordinates_cutoff=DEFAULT_COORDINATES_
return panel
def draw_deformed_cutoff_line(panel, image_depth, angle_degrees, coordinates_cutoff=DEFAULT_COORDINATES_CUTOFF):
panel = panel.copy()
line_y = cutoff_line_y(panel, image_depth, coordinates_cutoff)
if line_y is None:
return panel
theta = np.deg2rad(float(angle_degrees))
cos_t = np.cos(theta)
sin_t = np.sin(theta)
pivot_x = int(panel.width * 0.55)
pivot_y = int(panel.height * 0.62)
points = []
for x in (-panel.width * 0.08, panel.width * 1.08):
dx = x - pivot_x
dy = line_y - pivot_y
points.append((
pivot_x + cos_t * dx - sin_t * dy,
pivot_y + sin_t * dx + cos_t * dy,
))
draw = ImageDraw.Draw(panel)
shadow = (0, 0, 0)
line_color = (255, 215, 60)
draw.line(points, fill=shadow, width=6)
draw.line(points, fill=line_color, width=3)
return panel
def preview_deform_with_cutoff_line(
image,
image_depth,
angle_degrees,
mode="soft_transition",
transition_width=90,
gaussian_sigma=3,
show_cutoff_line=True,
coordinates_cutoff=DEFAULT_COORDINATES_CUTOFF,
):
preview = preview_deform_2d(
image,
angle_degrees,
mode,
transition_width=transition_width,
gaussian_sigma=gaussian_sigma,
)
if not show_cutoff_line:
return preview
return draw_deformed_cutoff_line(preview, image_depth, angle_degrees, coordinates_cutoff)
def fit_image(image, width, height):
scale = min(width / image.width, height / image.height)
resized = image.resize(
@@ -464,18 +522,34 @@ def make_four_state_preview(state_images, output_dir, angle_degrees, coordinates
screenshot_dir = output_dir / "process_screenshots"
reset_folder(screenshot_dir)
original_panel = sitk_sagittal_panel(state_images["original"], coordinates_cutoff)
image_depth = state_images["original"].GetSize()[2]
original_panel = sitk_sagittal_panel(state_images["original"])
original_display = (
draw_cutoff_line(original_panel, image_depth, coordinates_cutoff)
if coordinates_cutoff is not None
else original_panel
)
preview_panels = {
"original": original_panel,
"hard_boundary": preview_deform_2d(original_panel, angle_degrees, "hard_boundary"),
"gaussian_smooth": preview_deform_2d(
"original": original_display,
"hard_boundary": preview_deform_with_cutoff_line(
original_panel,
image_depth,
angle_degrees,
"hard_boundary",
show_cutoff_line=coordinates_cutoff is not None,
coordinates_cutoff=coordinates_cutoff or DEFAULT_COORDINATES_CUTOFF,
),
"gaussian_smooth": preview_deform_with_cutoff_line(
original_panel,
image_depth,
angle_degrees,
"gaussian_smooth",
transition_width,
show_cutoff_line=coordinates_cutoff is not None,
coordinates_cutoff=coordinates_cutoff or DEFAULT_COORDINATES_CUTOFF,
),
"soft_transition": preview_deform_2d(
original_panel,
original_display,
angle_degrees,
"soft_transition",
transition_width,

View File

@@ -28,6 +28,7 @@ from head_extension_app import (
draw_cutoff_line,
fit_image,
load_dicom_volume,
preview_deform_with_cutoff_line,
preview_deform_2d,
run_deformation,
sagittal_mip,
@@ -650,13 +651,24 @@ def make_preview(
volume = load_dicom_volume(input_dir)
before = crop_head_neck(sagittal_mip(volume))
before_display = draw_cutoff_line(before, volume.shape[0]) if show_cutoff_line else before
after = preview_deform_2d(
before_display,
float(angle_degrees),
mode,
transition_width=float(transition_width),
gaussian_sigma=float(gaussian_sigma),
)
if mode in {"hard_boundary", "gaussian_smooth"}:
after = preview_deform_with_cutoff_line(
before,
volume.shape[0],
float(angle_degrees),
mode,
transition_width=float(transition_width),
gaussian_sigma=float(gaussian_sigma),
show_cutoff_line=show_cutoff_line,
)
else:
after = preview_deform_2d(
before_display,
float(angle_degrees),
mode,
transition_width=float(transition_width),
gaussian_sigma=float(gaussian_sigma),
)
canvas_image = Image.new("RGB", (1440, 520), (0, 0, 0))
canvas_image.paste(fit_image(before_display, 700, 520), (0, 0))
@@ -666,15 +678,23 @@ def make_preview(
("Original", before_display),
(
"Hard boundary",
preview_deform_2d(before_display, float(angle_degrees), "hard_boundary"),
preview_deform_with_cutoff_line(
before,
volume.shape[0],
float(angle_degrees),
"hard_boundary",
show_cutoff_line=show_cutoff_line,
),
),
(
"Gaussian smooth",
preview_deform_2d(
before_display,
preview_deform_with_cutoff_line(
before,
volume.shape[0],
float(angle_degrees),
"gaussian_smooth",
gaussian_sigma=float(gaussian_sigma),
show_cutoff_line=show_cutoff_line,
),
),
(

View File

@@ -0,0 +1,52 @@
# 实现方案
开始时间2026-05-03-22-02-15
## 本次方案路径
`工程分析/实现方案-2026-05-03-22-02-15.md`
## 实现目标
将硬边界、高斯平滑预览图中的黄色分界线由形变后的 Y 字视觉改为单条线,同时不改变真实 DICOM 形变输出。
## 涉及文件
- `head_extension_app.py`
- `web_backend.py`,视调用兼容性决定是否需要小幅调整
- `工程分析/经验记录.md`
## 执行步骤
1.`head_extension_app.py` 中保留现有 `draw_cutoff_line`,用于原始图水平分界线。
2. 新增或调整一个预览线绘制辅助函数,用于在已经完成形变的预览图上叠加单条黄色线。
3. 修改快速预览生成逻辑:
- 原始图:仍按现有方式绘制水平分界线。
- 硬边界、高斯平滑:先对无黄线 CT 图执行 `preview_deform_2d`,再叠加单条斜向黄色分界线。
- 软过渡:保持现有视觉为单线;如实现上更简洁,可改为同一套“形变后叠线”的方式,但不得重新出现 Y 字。
4. 修改四状态过程预览生成逻辑,保证导出的 `process_comparison_4states.png``process_screenshots/hard_boundary.png``process_screenshots/gaussian_smooth.png` 中也是单条分界线。
5. 检查桌面预览入口是否复用同一逻辑,避免网页和桌面行为不一致。
6. 执行测试方案中的检查。
7. 修改完成后,将关键问题和解决方案追加到 `工程分析/经验记录.md`
8. 提交并推送 Gitea 备份commit 信息使用 `2026-05-03-22-02-15 调整预览分界线绘制`
9. 重新部署后端和前端服务到 `http://192.168.3.11:3005/`
## 预期实现细节
- 避免把带黄线的图片传入硬边界/高斯平滑的 `preview_deform_2d`
- 将黄线作为预览标注层绘制在形变结果上,使标注层不再被硬边界形变撕裂。
- 单条线的位置以当前分界线高度和旋转角度估算,保持和原有预览语义一致。
## 回滚思路
如效果不符合预期,可回滚本次对 `head_extension_app.py``web_backend.py` 的修改,恢复“先画线再形变”的旧逻辑;文档记录保留问题原因和回滚说明。
## 风险控制
- 不触碰 `run_deformation` 的真实 3D 形变字段生成逻辑。
- 不修改前端交互状态和接口参数,降低联动风险。
- 测试时覆盖快速预览接口、四状态过程图、前端类型检查和构建。
## 人工审核状态
待用户二次人工审核确认。未经确认不得修改业务代码。

View File

@@ -0,0 +1,77 @@
# 测试方案
开始时间2026-05-03-22-02-15
## 本次方案路径
`工程分析/测试方案-2026-05-03-22-02-15.md`
## 测试范围
- 快速预览接口 `/api/preview` 在硬边界、高斯平滑、软过渡三种模式下是否正常返回图片。
- 硬边界和高斯平滑预览图中黄色分界线是否为单条线。
- 四状态过程预览图中硬边界、高斯平滑截图是否不再出现 Y 字分界线。
- 前端类型检查和构建是否通过。
- 重新部署后 `http://192.168.3.11:3005/` 是否可访问。
## 测试命令
前端类型检查:
```bash
cd WebSite
npm run lint
```
前端构建:
```bash
cd WebSite
npm run build
```
Python 语法检查:
```bash
python -m py_compile head_extension_app.py web_backend.py
```
预览接口手工验证建议:
```bash
curl -s -X POST http://127.0.0.1:8787/api/preview \
-H 'Content-Type: application/json' \
-d '{"inputDir":"<有效DICOM目录>","angleDegrees":20,"showCutoffLine":true,"mode":"hard_boundary"}'
```
```bash
curl -s -X POST http://127.0.0.1:8787/api/preview \
-H 'Content-Type: application/json' \
-d '{"inputDir":"<有效DICOM目录>","angleDegrees":20,"showCutoffLine":true,"mode":"gaussian_smooth","gaussianSigma":3}'
```
## 手工验证点
- 在网页“影像变换工作站”打开预览分界线后,切换到“硬边界”,黄色线应为单条线。
- 切换到“高斯平滑”,黄色线应为单条线。
- 切换到“软过渡”,预览应仍自然、无 Y 字撕裂。
- 点击“隐藏预览分界线”后,预览图中不显示黄色线。
## 验收标准
- 硬边界、高斯平滑预览中不再出现 Y 字形黄色分界线。
- 预览接口仍正常返回图片。
- `python -m py_compile head_extension_app.py web_backend.py` 通过。
- `npm run lint` 通过。
- `npm run build` 通过。
- Gitea 文档和代码备份 commit 完成。
- 项目重新部署后 `http://192.168.3.11:3005/` 返回 `200 OK`
## 无法测试或残余风险
- 如果没有用户指定的实际业务 DICOM 数据,只能使用仓库现有样例目录 `Ori_Head_CT/` 做验证。
- 线条位置是预览标注层,和真实 3D DICOM 形变边界不应被理解为像素级临床标尺。
## 人工审核状态
待用户二次人工审核确认。未经确认不得修改业务代码。

View File

@@ -19,3 +19,21 @@ C. 解决问题方案
D. 后续如何避免问题
后续每次项目修改开始时先阅读 `AGENTS.md``工程分析/工程整体分析.md``工程分析/经验记录.md`,严格使用同一开始时间戳创建三类分析文档,并在用户确认实现方案和测试方案后再执行代码改动。
## 2026-05-03-22-02-15 调整硬边界与高斯平滑预览分界线
A. 具体问题
硬边界和高斯平滑预览图中的黄色分界线出现 Y 字形:一部分随头颈部形变变成斜线,另一部分仍横向延长,视觉上像多出一条不自然的横线。
B. 产生问题原因
旧逻辑先把黄色分界线画到原始 CT 预览图上,再把整张图传入 `preview_deform_2d` 做快速 2D 形变。硬边界和高斯平滑在分界附近存在不连续或快速变化的运动权重,导致同一条标注线被分裂成不同运动状态。
C. 解决问题方案
将硬边界和高斯平滑预览改为先对不含黄色线的 CT 图执行形变,再在形变结果上叠加单条黄色分界线。保留原始图的水平分界线,并尽量不改变软过渡原有视觉效果。
D. 后续如何避免问题
预览标注层、参考线、箭头等非 CT 内容应优先在形变结果上单独叠加,不要和医学影像像素一起参与形变采样。若必须参与形变,应先确认该标注能承受硬边界或快速变化权重造成的撕裂效果。

View File

@@ -0,0 +1,40 @@
# 需求分析
开始时间2026-05-03-22-02-15
## 原始需求
用户要求将“硬边界”和“高斯平滑”预览图上的黄色预览分界线,从当前的 Y 字形改成一条线。用户反馈当前横向延长部分过多,视觉上比较奇怪。
## 目标
- 硬边界预览图中的黄色分界线显示为单条线,不再出现一条斜线叠加一条长横线的 Y 字效果。
- 高斯平滑预览图中的黄色分界线显示为单条线,不再出现楔形或 Y 字残留。
- 保留“显示/隐藏预览分界线”的现有功能。
- 不改变 DICOM 形变计算结果,只调整预览分界线的绘制方式。
## 影响范围
- 主要影响 `head_extension_app.py` 中的预览图绘制逻辑。
- `web_backend.py` 调用 `preview_deform_2d``draw_cutoff_line` 的路径会间接受影响,必要时同步调整调用方式。
- 前端 `WebSite/src/App.tsx` 预计不需要修改,因为问题发生在后端生成的预览图片中。
## 当前定位
当前预览分界线由 `draw_cutoff_line` 先画到原始 sagittal panel 上,再将带黄线的图片传给 `preview_deform_2d`。硬边界和高斯平滑模式下,分界线附近的运动权重不连续或变化很快,导致黄线的一部分随头部旋转,另一部分保持水平,从而形成 Y 字形。
## 约束
- 修改前必须先完成实现方案和测试方案文档,并等待用户二次人工审核确认。
- 本次应聚焦预览线绘制,不改动真实 3D DICOM 形变算法。
- 需要避免让原图、软过渡预览或下载结果出现新的不一致。
## 风险点
- 如果直接改变 `preview_deform_2d` 的形变算法,可能影响预览图中 CT 形变效果,超出需求范围。
- 如果只隐藏横线,可能导致用户无法识别分界位置。
- 如果新线条绘制与图片缩放顺序不一致,可能在网页预览和四状态过程图里出现线条位置偏差。
## 待确认事项
建议方案是:硬边界和高斯平滑预览先生成无黄线的形变 CT 图,再在形变结果上叠加单条黄色斜向分界线;原始图仍显示水平线,软过渡维持当前单线效果或使用同一叠加方式保持一致。需用户确认后执行代码修改。