2026-05-03-22-02-15 调整预览分界线绘制
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
(
|
||||
|
||||
52
工程分析/实现方案-2026-05-03-22-02-15.md
Normal file
52
工程分析/实现方案-2026-05-03-22-02-15.md
Normal 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 形变字段生成逻辑。
|
||||
- 不修改前端交互状态和接口参数,降低联动风险。
|
||||
- 测试时覆盖快速预览接口、四状态过程图、前端类型检查和构建。
|
||||
|
||||
## 人工审核状态
|
||||
|
||||
待用户二次人工审核确认。未经确认不得修改业务代码。
|
||||
77
工程分析/测试方案-2026-05-03-22-02-15.md
Normal file
77
工程分析/测试方案-2026-05-03-22-02-15.md
Normal 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 形变边界不应被理解为像素级临床标尺。
|
||||
|
||||
## 人工审核状态
|
||||
|
||||
待用户二次人工审核确认。未经确认不得修改业务代码。
|
||||
18
工程分析/经验记录.md
18
工程分析/经验记录.md
@@ -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 内容应优先在形变结果上单独叠加,不要和医学影像像素一起参与形变采样。若必须参与形变,应先确认该标注能承受硬边界或快速变化权重造成的撕裂效果。
|
||||
|
||||
40
工程分析/需求分析-2026-05-03-22-02-15.md
Normal file
40
工程分析/需求分析-2026-05-03-22-02-15.md
Normal 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 图,再在形变结果上叠加单条黄色斜向分界线;原始图仍显示水平线,软过渡维持当前单线效果或使用同一叠加方式保持一致。需用户确认后执行代码修改。
|
||||
Reference in New Issue
Block a user