toggle cutoff line only in quick preview
This commit is contained in:
@@ -31,6 +31,8 @@ import {
|
|||||||
Server,
|
Server,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Info,
|
Info,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
X
|
X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
@@ -239,6 +241,7 @@ export default function App() {
|
|||||||
// --- Simulation State (Workspace) ---
|
// --- Simulation State (Workspace) ---
|
||||||
const [cervicalRotation, setCervicalRotation] = useState(14.5);
|
const [cervicalRotation, setCervicalRotation] = useState(14.5);
|
||||||
const [transitionWidth, setTransitionWidth] = useState(90);
|
const [transitionWidth, setTransitionWidth] = useState(90);
|
||||||
|
const [showPreviewCutoffLine, setShowPreviewCutoffLine] = useState(true);
|
||||||
const [isSimulating, setIsSimulating] = useState(restoredDeformationJob?.job.status === 'running');
|
const [isSimulating, setIsSimulating] = useState(restoredDeformationJob?.job.status === 'running');
|
||||||
const [progress, setProgress] = useState(restoredDeformationJob ? progressFromJob(restoredDeformationJob.job, restoredDeformationJob.progress) : 0);
|
const [progress, setProgress] = useState(restoredDeformationJob ? progressFromJob(restoredDeformationJob.job, restoredDeformationJob.progress) : 0);
|
||||||
const [toastMessage, setToastMessage] = useState("");
|
const [toastMessage, setToastMessage] = useState("");
|
||||||
@@ -665,7 +668,8 @@ export default function App() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
inputDir: selectedInputDir,
|
inputDir: selectedInputDir,
|
||||||
angleDegrees: cervicalRotation
|
angleDegrees: cervicalRotation,
|
||||||
|
showCutoffLine: showPreviewCutoffLine
|
||||||
}),
|
}),
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
});
|
});
|
||||||
@@ -688,7 +692,7 @@ export default function App() {
|
|||||||
controller.abort();
|
controller.abort();
|
||||||
window.clearTimeout(timer);
|
window.clearTimeout(timer);
|
||||||
};
|
};
|
||||||
}, [currentPage, selectedInputDir, cervicalRotation]);
|
}, [currentPage, selectedInputDir, cervicalRotation, showPreviewCutoffLine]);
|
||||||
|
|
||||||
const handleRunSimulation = async () => {
|
const handleRunSimulation = async () => {
|
||||||
if (isSimulating) return;
|
if (isSimulating) return;
|
||||||
@@ -901,6 +905,17 @@ export default function App() {
|
|||||||
<div className="flex justify-between text-xs font-bold mb-2 text-slate-600"><span>过渡平滑宽度</span><span className="text-blue-600 font-mono">{transitionWidth}</span></div>
|
<div className="flex justify-between text-xs font-bold mb-2 text-slate-600"><span>过渡平滑宽度</span><span className="text-blue-600 font-mono">{transitionWidth}</span></div>
|
||||||
<input type="range" min="50" max="160" step="10" value={transitionWidth} onChange={e => setTransitionWidth(parseInt(e.target.value, 10))} className="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600 opacity-80 hover:opacity-100 transition-opacity" />
|
<input type="range" min="50" max="160" step="10" value={transitionWidth} onChange={e => setTransitionWidth(parseInt(e.target.value, 10))} className="w-full h-1.5 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-blue-600 opacity-80 hover:opacity-100 transition-opacity" />
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPreviewCutoffLine(value => !value)}
|
||||||
|
className={`w-full py-2.5 rounded-xl text-xs font-black transition-all flex items-center justify-center gap-2 border ${
|
||||||
|
showPreviewCutoffLine
|
||||||
|
? 'bg-yellow-50 text-yellow-700 border-yellow-200 hover:bg-yellow-100'
|
||||||
|
: 'bg-slate-50 text-slate-500 border-slate-200 hover:bg-slate-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{showPreviewCutoffLine ? <Eye size={14} /> : <EyeOff size={14} />}
|
||||||
|
{showPreviewCutoffLine ? '隐藏预览分界线' : '显示预览分界线'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -355,18 +355,12 @@ def run_deformation(input_dir, output_dir, angle_degrees, transition_width, prog
|
|||||||
output_paths["legacy_soft"] = legacy_soft_dir
|
output_paths["legacy_soft"] = legacy_soft_dir
|
||||||
|
|
||||||
progress("正在生成四状态过程对比图...")
|
progress("正在生成四状态过程对比图...")
|
||||||
preview_paths = make_four_state_preview(
|
preview_paths = make_four_state_preview(state_images, Path(output_dir), angle_degrees)
|
||||||
state_images,
|
|
||||||
Path(output_dir),
|
|
||||||
angle_degrees,
|
|
||||||
vol_info.coordinates_cutoff,
|
|
||||||
)
|
|
||||||
make_output_preview_from_images(
|
make_output_preview_from_images(
|
||||||
state_images["original"],
|
state_images["original"],
|
||||||
state_images["soft_transition"],
|
state_images["soft_transition"],
|
||||||
Path(output_dir),
|
Path(output_dir),
|
||||||
angle_degrees,
|
angle_degrees,
|
||||||
vol_info.coordinates_cutoff,
|
|
||||||
)
|
)
|
||||||
return output_paths, preview_paths
|
return output_paths, preview_paths
|
||||||
|
|
||||||
@@ -604,12 +598,12 @@ class HeadExtensionApp:
|
|||||||
self.status.set("正在读取 DICOM 生成预览...")
|
self.status.set("正在读取 DICOM 生成预览...")
|
||||||
self.cached_volume = load_dicom_volume(self.input_dir.get())
|
self.cached_volume = load_dicom_volume(self.input_dir.get())
|
||||||
before = crop_head_neck(sagittal_mip(self.cached_volume))
|
before = crop_head_neck(sagittal_mip(self.cached_volume))
|
||||||
after = preview_deform_2d(before, float(self.angle.get()))
|
before_with_line = draw_cutoff_line(before, self.cached_volume.shape[0])
|
||||||
after = draw_cutoff_line(after, self.cached_volume.shape[0])
|
after = preview_deform_2d(before_with_line, float(self.angle.get()))
|
||||||
|
|
||||||
canvas = Image.new("RGB", (1120, 610), (0, 0, 0))
|
canvas = Image.new("RGB", (1120, 610), (0, 0, 0))
|
||||||
draw = ImageDraw.Draw(canvas)
|
draw = ImageDraw.Draw(canvas)
|
||||||
before_panel = fit_image(before, 520, 500)
|
before_panel = fit_image(before_with_line, 520, 500)
|
||||||
after_panel = fit_image(after, 520, 500)
|
after_panel = fit_image(after, 520, 500)
|
||||||
canvas.paste(before_panel, (30, 80))
|
canvas.paste(before_panel, (30, 80))
|
||||||
canvas.paste(after_panel, (570, 80))
|
canvas.paste(after_panel, (570, 80))
|
||||||
|
|||||||
@@ -547,15 +547,18 @@ def start_job(kind, worker, owner=None, params=None, remember_user_task=True):
|
|||||||
return get_job(job_id)
|
return get_job(job_id)
|
||||||
|
|
||||||
|
|
||||||
def make_preview(input_dir, angle_degrees):
|
def make_preview(input_dir, angle_degrees, show_cutoff_line=True):
|
||||||
volume = load_dicom_volume(input_dir)
|
volume = load_dicom_volume(input_dir)
|
||||||
before = crop_head_neck(sagittal_mip(volume))
|
before = crop_head_neck(sagittal_mip(volume))
|
||||||
after = preview_deform_2d(before, float(angle_degrees))
|
before_display = draw_cutoff_line(before, volume.shape[0]) if show_cutoff_line else before
|
||||||
after = draw_cutoff_line(after, volume.shape[0])
|
after = preview_deform_2d(before_display, float(angle_degrees))
|
||||||
|
|
||||||
|
canvas_image = Image.new("RGB", (1440, 520), (0, 0, 0))
|
||||||
|
canvas_image.paste(fit_image(before_display, 700, 520), (0, 0))
|
||||||
|
canvas_image.paste(fit_image(after, 700, 520), (740, 0))
|
||||||
|
|
||||||
canvas = BytesIO()
|
canvas = BytesIO()
|
||||||
preview = fit_image(after, 720, 520)
|
canvas_image.save(canvas, format="PNG")
|
||||||
preview.save(canvas, format="PNG")
|
|
||||||
encoded = base64.b64encode(canvas.getvalue()).decode("ascii")
|
encoded = base64.b64encode(canvas.getvalue()).decode("ascii")
|
||||||
return {
|
return {
|
||||||
"image": f"data:image/png;base64,{encoded}",
|
"image": f"data:image/png;base64,{encoded}",
|
||||||
@@ -721,7 +724,13 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
body = self.read_json()
|
body = self.read_json()
|
||||||
if parsed.path == "/api/preview":
|
if parsed.path == "/api/preview":
|
||||||
self.send_json(make_preview(body["inputDir"], body.get("angleDegrees", 12)))
|
self.send_json(
|
||||||
|
make_preview(
|
||||||
|
body["inputDir"],
|
||||||
|
body.get("angleDegrees", 12),
|
||||||
|
bool(body.get("showCutoffLine", True)),
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if parsed.path == "/api/deformation/package":
|
if parsed.path == "/api/deformation/package":
|
||||||
|
|||||||
Reference in New Issue
Block a user