Counterfactual PGD attack on MedGS / 3DGS Gaussian parameters using gradients from the Sybil lung-cancer risk classifier.
MedGS is included as a git submodule, so clone recursively:
git clone --recurse-submodules https://github.com/gmum/COGENT.git- Linux + NVIDIA GPU
- Conda installed
- This repository cloned with submodules available
Run from repository root:
bash setup.shThis script:
- updates MedGS submodules,
- creates/updates the
sybil_interpretabilityconda env, - installs PyTorch + CUDA packages,
- builds MedGS CUDA extensions (
diff-gaussian-rasterization,fused-ssim,simple-knn).
Sybil weights are not bundled with this repository. They are public (MIT license) and published as a GitHub release asset:
mkdir -p data/weights/sybil/checkpoints
curl -L -o /tmp/sybil_checkpoints.zip \
https://github.com/reginabarzilaygroup/Sybil/releases/download/v1.5.0/sybil_checkpoints.zip
unzip /tmp/sybil_checkpoints.zip -d /tmp/sybil_ckptThe archive ships the five ensemble members named by md5 hash (*.ckpt) plus the
calibrators. Map them to the names this repo expects:
| file in release | file here |
|---|---|
28a7cd44f5bcd3e6cc760b65c7e0d54d.ckpt |
sybil_0.pt |
56ce1a7d241dc342982f5466c4a9d7ef.ckpt |
sybil_1.pt |
624407ef8e3a2a009f9fa51f9846fe9a.ckpt |
sybil_2.pt |
64a91b25f84141d32852e75a3aec7305.ckpt |
sybil_3.pt |
65fd1f04cb4c5847d86a9ed8ba31ac1a.ckpt |
sybil_4.pt |
sybil_ensemble_simple_calibrator.json is already named correctly — copy it as is.
Final layout:
data/weights/sybil/checkpoints/
├── sybil_0.pt
├── sybil_1.pt
├── sybil_2.pt
├── sybil_3.pt
├── sybil_4.pt
└── sybil_ensemble_simple_calibrator.json
Mirrors: the same files are on the Google Drive folder linked in the
Sybil README, and the sybil
package will fetch them into ~/.sybil automatically via Sybil("sybil_ensemble").
Weights and code from Mikhael et al., Sybil: A Validated Deep Learning Model to Predict Future Lung Cancer Risk From a Single Low-Dose Chest CT, JCO 2023.
conda activate sybil_interpretability
export MEDGS_ROOT="$PWD/submodules/MedGS"
export PYTHONPATH="$PWD/submodules/Sybil/src:$PWD/submodules/Sybil/src_sybil:$PWD/submodules/MedGS:$PYTHONPATH"Example command used in this workspace:
python main.py \
--model_path data/1/1 \
--source_path data/1 \
--masks data/1/masks \
--output_ply data/1/1/attacked_clf.ply \
--sigma 0.5 --remove_top_pct 10.0 \
--props f_dc_0 f_dc_1 f_dc_2 \
--eps 0.5 --steps 10 \
--attack_mode untargeted \
--classifier_config_dir "$PWD/submodules/Sybil/configs" \
--classifier_config_name nlst_sybil_ensemble_inference \
--target_year 5 \
--max_slices 60The attacked Gaussian point cloud is written to:
data/1/1/attacked_clf.ply
The analysis compares attacked renders vs clean renders and extracts connected components inside lungs.
cp -r data/1/1 data/1/1_attacked_clf
cp data/1/1/attacked_clf.ply data/1/1_attacked_clf/point_cloud/iteration_30000/point_cloud.ply
python submodules/MedGS/render.py \
--model_path data/1/1_attacked_clf \
--source_path data/1 \
--iteration 30000 \
--camera mirror \
--pipeline imgClean renders are read from data/1/1/render_img and attacked renders from
data/1/1_attacked_clf/render_img.
python -c "from analysis.image_diff import generate_image_diffs as f; f('data/1/1_attacked_clf/render_img','data/1/1/render_img','analysis/run1/diffs')"python -c "from analysis.component_analysis import analyze_connected_components as f; f(diff_folder='analysis/run1/diffs', output_folder='analysis/run1/components', lung_masks_folder='data/1/lung_masks', threshold=None, min_area=20, closing_radius=5)"Outputs:
analysis/run1/components/centroids.csvanalysis/run1/components/masks/analysis/run1/components/visualizations/
python -c "from analysis.overlay_circles import draw_circles_on_originals as f; f(csv_path='analysis/run1/components/centroids.csv', originals_folder='data/1/original', output_folder='analysis/run1/annotated', radius_mode='area', scale=1.5, min_radius=10, thickness=2, color=(255,0,0))"alternatively the full pipeline can by run via
$ bash run_patient_pipeline.sh <patient_id>.
├── attack/ attack pipeline
│ ├── main.py entry point
│ ├── cli.py command-line arguments
│ ├── constants.py Sybil / MedGS constants
│ ├── medgs_bootstrap.py finds MedGS on sys.path
│ ├── ply_io.py raw PLY reading + SH-degree detection
│ ├── geometry.py camera matrices, 3D covariances, projection
│ ├── camera_loader.py matches cameras.json with mask files
│ ├── mask_filtering.py selects Gaussians lying inside masks
│ ├── attribute_mapping.py PLY fields <-> GaussianModel tensor slices
│ ├── classifier_loader.py Sybil ensemble (frozen weights)
│ ├── volume_rendering.py renders slices into a Sybil-shaped volume
│ ├── pgd_attack.py PGD loop on Gaussian attributes
│ └── scene_setup.py MedGS Scene initialization
│
└── analysis/ post-attack analysis
├── image_diff.py per-pixel absolute diffs between two folders
├── component_analysis.py connected components inside the lung masks
└── overlay_circles.py draws circles on originals from centroid CSV
- Train MedGS on a CT volume.
- Run
python main.py ...to generate an attacked PLY. - Render attacked slices with
submodules/MedGS/render.py. - Run
analysis/image_diff.pyto create attacked-vs-clean diffs. - Run
analysis/component_analysis.pyto keep components inside lungs. - Run
analysis/overlay_circles.pyto annotate originals.

Figure 1. 3D representation of lungs with a tumor.

Figure 2. Comparison of nodules before and after the attack, including physician annotations (a plus sign indicates positive changes regarding prognosis, a minus sign indicates negative changes, and the number indicates severity).