├── .editorconfig ├── .github └── workflows │ ├── build_exe.yaml.bak │ ├── release.yaml │ └── tests.yaml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── class_diagram.md ├── docs ├── 1_overview.html ├── 2_deidentification protocol.html ├── 3_managing anonymization projects.html ├── 4_project settings.html ├── 5_operation.html ├── 6_license.html ├── images │ ├── AWSCognitoCredentials.png │ ├── CloneProject.png │ ├── CloseProject.png │ ├── Dashboard.png │ ├── ExportServer.png │ ├── ExportStudiesAWS.png │ ├── ExportStudiesStatus.png │ ├── ImportFilesDialog.png │ ├── ImportFilesMenu.png │ ├── ImportStudiesDialog.png │ ├── ImportStudiesResult.png │ ├── LocalServer.png │ ├── LoggingLevels.png │ ├── Modalities.png │ ├── NetworkTimeouts.png │ ├── NewProject.png │ ├── NewProjectSettings.png │ ├── OpenLogViewer.png │ ├── OpenRecent.png │ ├── PatientLookupData.png │ ├── PatientLookupSave.png │ ├── QueryRetrieveImport.png │ ├── QueryServer.png │ ├── SelectDirectory.png │ ├── SelectFiles.png │ ├── SelectQuery.png │ ├── StorageClasses.png │ ├── StorageDirectory.png │ ├── TransferSyntaxes.png │ ├── Welcome_en_osx_light.png │ └── Welcome_en_win_light.png ├── index.html ├── refs │ ├── AttributeLevelConfidentiality_sup55.pdf │ ├── CTP-update-notes_jperry.pdf │ ├── Issues │ │ └── c-move_vna.txt │ ├── ModalityForReportsRequestDocs_cp749_lb.pdf │ └── RSNA-Covid-19-Deidentification-Protocol.pdf └── styles.css ├── patch_release.sh ├── poetry.lock ├── pyproject.toml ├── readme.de.md ├── readme.es.md ├── readme.fr.md ├── readme.md ├── src ├── anonymizer │ ├── __init__.py │ ├── anonymizer.py │ ├── assets │ │ ├── fonts │ │ │ └── DINAlternate-Bold.ttf │ │ ├── icons │ │ │ ├── create_icns.sh │ │ │ ├── kaleidoscope.png │ │ │ ├── kaleidoscope_flat.svg │ │ │ ├── kaleidoscope_stack_angle.svg │ │ │ ├── kaleidoscope_stack_vert.svg │ │ │ ├── pause.png │ │ │ ├── play.png │ │ │ ├── rsna_icon.icns │ │ │ ├── rsna_icon.ico │ │ │ ├── rsna_icon.png │ │ │ ├── rsna_logo_alpha.png │ │ │ ├── rsna_logo_head_profile.png │ │ │ ├── rsna_logo_head_profile_1024x1024.png │ │ │ ├── rsna_logo_head_profile_titled.png │ │ │ ├── rsna_titled_logo.svg │ │ │ ├── rsna_titled_logo_alpha.png │ │ │ └── signpath_logo.svg │ │ ├── locales │ │ │ ├── Pipfile │ │ │ ├── de │ │ │ │ ├── LC_MESSAGES │ │ │ │ │ ├── messages.mo │ │ │ │ │ ├── messages.po │ │ │ │ │ └── messages.po~ │ │ │ │ ├── html │ │ │ │ │ ├── 1_übersicht.html │ │ │ │ │ ├── 2_deidentifikationsprotokoll.html │ │ │ │ │ ├── 3_anonymisierungsprojekte_verwalten.html │ │ │ │ │ ├── 4_projekteinstellungen.html │ │ │ │ │ ├── 5_bedienung.html │ │ │ │ │ ├── 6_lizenz.html │ │ │ │ │ └── images │ │ │ │ │ │ ├── AWSCognitoCredentials.png │ │ │ │ │ │ ├── AbfrageAbrufenImportieren.png │ │ │ │ │ │ ├── Auswahlabfrage.png │ │ │ │ │ │ ├── BildarchivQRServer.png │ │ │ │ │ │ ├── Dashboard.png │ │ │ │ │ │ ├── Dateiauswahloptionen.png │ │ │ │ │ │ ├── DateienImportieren.png │ │ │ │ │ │ ├── ExportServer.png │ │ │ │ │ │ ├── ExportStudienAWS.png │ │ │ │ │ │ ├── ExportStudienStatus.png │ │ │ │ │ │ ├── ImportlokalenDateisystem.png │ │ │ │ │ │ ├── KlonenProjekts.png │ │ │ │ │ │ ├── LokalerServer.png │ │ │ │ │ │ ├── Modalitäten.png │ │ │ │ │ │ ├── Netzwerk-Zeitüberschreitungen.png │ │ │ │ │ │ ├── NeueProjekteinstellungen.png │ │ │ │ │ │ ├── NeuesProjekt.png │ │ │ │ │ │ ├── Patienten-Such-CSV-Datei.png │ │ │ │ │ │ ├── PatientenPHI.png │ │ │ │ │ │ ├── Projektschließen.png │ │ │ │ │ │ ├── Protokollierungsstufen.png │ │ │ │ │ │ ├── Speicherklassen.png │ │ │ │ │ │ ├── Speicherverzeichnis.png │ │ │ │ │ │ ├── TransferSyntaxen.png │ │ │ │ │ │ ├── VerzeichnisAuswählen.png │ │ │ │ │ │ ├── Welcome_de_osx_light.png │ │ │ │ │ │ ├── Welcome_de_win_light.png │ │ │ │ │ │ └── WiedereröffnenProjekts.png │ │ │ │ └── whitelists │ │ │ │ │ ├── ct.txt │ │ │ │ │ ├── dx.txt │ │ │ │ │ ├── mr.txt │ │ │ │ │ └── us.txt │ │ │ ├── en_US │ │ │ │ ├── LC_MESSAGES │ │ │ │ │ ├── messages.mo │ │ │ │ │ ├── messages.po │ │ │ │ │ └── messages.po~ │ │ │ │ ├── html │ │ │ │ │ ├── 1_overview.html │ │ │ │ │ ├── 2_deidentification protocol.html │ │ │ │ │ ├── 3_managing anonymization projects.html │ │ │ │ │ ├── 4_project settings.html │ │ │ │ │ ├── 5_operation.html │ │ │ │ │ ├── 6_license.html │ │ │ │ │ └── images │ │ │ │ │ │ ├── AWSCognitoCredentials.png │ │ │ │ │ │ ├── CloneProject.png │ │ │ │ │ │ ├── CloseProject.png │ │ │ │ │ │ ├── Dashboard.png │ │ │ │ │ │ ├── ExportServer.png │ │ │ │ │ │ ├── ExportStudiesAWS.png │ │ │ │ │ │ ├── ExportStudiesStatus.png │ │ │ │ │ │ ├── ImportFilesDialog.png │ │ │ │ │ │ ├── ImportFilesMenu.png │ │ │ │ │ │ ├── ImportStudiesDialog.png │ │ │ │ │ │ ├── ImportStudiesResult.png │ │ │ │ │ │ ├── LocalServer.png │ │ │ │ │ │ ├── LoggingLevels.png │ │ │ │ │ │ ├── Modalities.png │ │ │ │ │ │ ├── NetworkTimeouts.png │ │ │ │ │ │ ├── NewProject.png │ │ │ │ │ │ ├── NewProjectSettings.png │ │ │ │ │ │ ├── OpenLogViewer.png │ │ │ │ │ │ ├── OpenRecent.png │ │ │ │ │ │ ├── PatientLookupData.png │ │ │ │ │ │ ├── PatientLookupSave.png │ │ │ │ │ │ ├── QueryRetrieveImport.png │ │ │ │ │ │ ├── QueryServer.png │ │ │ │ │ │ ├── SelectDirectory.png │ │ │ │ │ │ ├── SelectFiles.png │ │ │ │ │ │ ├── SelectQuery.png │ │ │ │ │ │ ├── StorageClasses.png │ │ │ │ │ │ ├── StorageDirectory.png │ │ │ │ │ │ ├── TransferSyntaxes.png │ │ │ │ │ │ ├── Welcome_en_osx_light.png │ │ │ │ │ │ └── Welcome_en_win_light.png │ │ │ │ └── whitelists │ │ │ │ │ ├── ct.txt │ │ │ │ │ ├── dx.txt │ │ │ │ │ ├── mr.txt │ │ │ │ │ └── us.txt │ │ │ ├── es │ │ │ │ ├── LC_MESSAGES │ │ │ │ │ ├── messages.mo │ │ │ │ │ ├── messages.po │ │ │ │ │ └── messages.po~ │ │ │ │ ├── html │ │ │ │ │ ├── 1_visión general.html │ │ │ │ │ ├── 2_protocolo de desidentificación.html │ │ │ │ │ ├── 3_gestión de proyectos.html │ │ │ │ │ ├── 4_configuración del proyecto.html │ │ │ │ │ ├── 5_operación.html │ │ │ │ │ ├── 6_licencia.html │ │ │ │ │ └── images │ │ │ │ │ │ ├── AWSCognitoCredenciales.png │ │ │ │ │ │ ├── AbrirReciente.png │ │ │ │ │ │ ├── CerrarProyecto.png │ │ │ │ │ │ ├── ClasesDeAlmacenamiento.png │ │ │ │ │ │ ├── ClonaciónDeProyecto.png │ │ │ │ │ │ ├── ConfiguraciónDelProyecto.png │ │ │ │ │ │ ├── ConsultarRecuperarImportar.png │ │ │ │ │ │ ├── DatosDeBúsquedaDePaciente.png │ │ │ │ │ │ ├── DirectorioDeAlmacenamiento.png │ │ │ │ │ │ ├── ExportarEstudiosAWS.png │ │ │ │ │ │ ├── ExportarEstudiosEstado.png │ │ │ │ │ │ ├── GuardarBúsquedaDePaciente.png │ │ │ │ │ │ ├── ImportarArchivos.png │ │ │ │ │ │ ├── ImportarArchivosDiálogo.png │ │ │ │ │ │ ├── ImportarEstudiosDiálogo.png │ │ │ │ │ │ ├── ImportarEstudiosResultado.png │ │ │ │ │ │ ├── Modalidades.png │ │ │ │ │ │ ├── NivelesDeRegistro.png │ │ │ │ │ │ ├── NuevoProyecto.png │ │ │ │ │ │ ├── PanelDeControl.png │ │ │ │ │ │ ├── SeleccionarArchivos.png │ │ │ │ │ │ ├── SeleccionarDirectorio.png │ │ │ │ │ │ ├── SeleccionarImportar.png │ │ │ │ │ │ ├── ServidorDeConsulta.png │ │ │ │ │ │ ├── ServidorDeExportación.png │ │ │ │ │ │ ├── ServidorLocal.png │ │ │ │ │ │ ├── SintaxisDeTransferencia.png │ │ │ │ │ │ ├── TimeoutsDeRed.png │ │ │ │ │ │ ├── Welcome_es_osx_light.png │ │ │ │ │ │ └── Welcome_es_win_light.png │ │ │ │ ├── html_bak │ │ │ │ │ ├── 1_visión general.html │ │ │ │ │ ├── 2_protocolo de desidentificación.html │ │ │ │ │ ├── 3_gestión de proyectos.html │ │ │ │ │ ├── 4_configuración del proyecto.html │ │ │ │ │ ├── 5_operación.html │ │ │ │ │ └── 6_licencia.html │ │ │ │ └── whitelists │ │ │ │ │ ├── ct.txt │ │ │ │ │ ├── dx.txt │ │ │ │ │ ├── mr.txt │ │ │ │ │ └── us.txt │ │ │ ├── extract_translations.sh │ │ │ ├── fr │ │ │ │ ├── LC_MESSAGES │ │ │ │ │ ├── messages.mo │ │ │ │ │ ├── messages.po │ │ │ │ │ └── messages.po~ │ │ │ │ ├── html │ │ │ │ │ ├── 1_aperçu.html │ │ │ │ │ ├── 2_protocole de désidentification.html │ │ │ │ │ ├── 3_gestion de projet.html │ │ │ │ │ ├── 4_paramètres du projet.html │ │ │ │ │ ├── 5_opération.html │ │ │ │ │ ├── 6_licence.html │ │ │ │ │ └── images │ │ │ │ │ │ ├── Welcome_fr_osx_light.png │ │ │ │ │ │ └── Welcome_fr_win_light.png │ │ │ │ └── whitelists │ │ │ │ │ ├── ct.txt │ │ │ │ │ ├── dx.txt │ │ │ │ │ ├── mr.txt │ │ │ │ │ └── us.txt │ │ │ ├── messages.pot │ │ │ └── update_translations.sh │ │ ├── scripts │ │ │ └── default-anonymizer.script │ │ └── themes │ │ │ ├── dark-blue.json │ │ │ └── rsna_theme.json │ ├── controller │ │ ├── __init__.py │ │ ├── anonymizer.py │ │ ├── create_projections.py │ │ ├── dicom_C_codes.py │ │ ├── project.py │ │ └── remove_pixel_phi.py │ ├── model │ │ ├── __init__.py │ │ ├── anonymizer.py │ │ └── project.py │ ├── utils │ │ ├── __init__.py │ │ ├── logging.py │ │ ├── modalities.py │ │ ├── network.py │ │ ├── storage.py │ │ ├── translate.py │ │ └── version.py │ └── view │ │ ├── __init__.py │ │ ├── dashboard.py │ │ ├── delete_studies_dialog.py │ │ ├── export.py │ │ ├── histogram.py │ │ ├── html_view.py │ │ ├── image.py │ │ ├── import_files_dialog.py │ │ ├── import_studies_dialog.py │ │ ├── index.py │ │ ├── projection.py │ │ ├── query_retrieve_import.py │ │ ├── series.py │ │ ├── settings │ │ ├── aws_cognito_dialog.py │ │ ├── dicom_node_dialog.py │ │ ├── logging_levels_dialog.py │ │ ├── modalites_dialog.py │ │ ├── network_timeouts_dialog.py │ │ ├── settings_dialog.py │ │ ├── sop_classes_dialog.py │ │ └── transfer_syntaxes_dialog.py │ │ ├── ux_fields.py │ │ └── welcome.py └── prototyping │ ├── __init__.py │ ├── analyse_h264_dcm.py │ ├── anonymizer_stripped.py │ ├── async_echo.py │ ├── asyncio_ttk.py │ ├── asyncio_with_queue.py │ ├── aws_export1.py │ ├── aws_export2.py │ ├── aws_import1.py │ ├── build.py │ ├── build_stripped.py │ ├── complex_eg.py │ ├── config.py │ ├── contact_sheet_ctk.py │ ├── contact_sheet_gif.py │ ├── contact_sheet_matplotlib.py │ ├── contact_sheet_scroll_frame.py │ ├── contact_sheet_tk.py │ ├── ctk_tkphotoimage_stack_resizable.py │ ├── ctk_toplevel.py │ ├── ctkinputdlg.py │ ├── ctksliderwithmarkers.py │ ├── dataclass_json.py │ ├── east_dnn1.py │ ├── epic_fhir_1.py │ ├── epic_fhir_2.py │ ├── epic_fhir_3.py │ ├── epic_fhir_4.py │ ├── ffmpeg_xa.py │ ├── get_series_uids.py │ ├── hapi_fhir_2.py │ ├── histogram_widget_1.py │ ├── histogram_widget_2.py │ ├── html_helloworld.py │ ├── html_instructions.py │ ├── imageviewer_1_ctk.py │ ├── imageviewer_2_tk.py │ ├── kaleidoscope_no_lazyload.py │ ├── kaleidoscope_scroller.py │ ├── lazy_load_image_scroll_ctkSF.py │ ├── lazy_load_image_scroll_tk.py │ ├── locale.py │ ├── matplotlib_3d.py │ ├── modalityLUT_smpte_test_pattern.py │ ├── ocr1.py │ ├── ocr2_ner.py │ ├── ocr3_anon_blacked.py │ ├── ocr4_anon_rect_mask_inpaint.py │ ├── ocr5_anon_contour_mask_inpaint.py │ ├── ocr6_deid_ner_rect_mask_inpaint.py │ ├── ocr7_deid_ner_contour_mask_inpaint.py │ ├── ocr8_deid_ner_contour_mask_inpaint_grayscale_dcm.py │ ├── ocr9_deid_ner_contour_mask_inpaint_RGB_dcm.py │ ├── parse_log.py │ ├── pause.png │ ├── pixels_ctk.py │ ├── play.png │ ├── progress_dialog.py │ ├── pynetdicom_scp.py │ ├── radon_raw_totals.py │ ├── remove_facial_features.py │ ├── set_gha_env.py │ ├── storage_dir.py │ ├── test_translation.py │ ├── threadpooldemo.py │ ├── tkcalendar.py │ ├── tkinter_toplevel.py │ ├── tkmenutest1.py │ ├── tkmenutest2.py │ ├── tkmenutest3.py │ ├── tkphotoimage_stack video_status_below.py │ ├── tkphotoimage_stack video_status_overlay.py │ ├── tkphotoimage_stack.py │ ├── treeviewtable_eg.py │ ├── ttktreeview.py │ ├── ttktreeview2.py │ └── ttktreeview_popup.py └── tests ├── __init__.py ├── conftest.py └── controller ├── assets ├── JavaGeneratedIndex.xlsx └── test_dcm_files │ ├── JPEG-LS_Lossy.dcm │ ├── dicomdirtests_README.txt │ └── pydicom_test_files.txt ├── dicom_pacs_simulator_scp.py ├── dicom_test_files.py ├── dicom_test_nodes.py ├── helpers.py ├── test_anonymize.py ├── test_aws_upload.py ├── test_create_projections.py ├── test_dicom_echo_scu.py ├── test_dicom_move_scu.py ├── test_dicom_send_find_scu.py ├── test_dicom_storage_scp.py ├── test_load_java_index.py ├── test_logging.py ├── test_modalities.py ├── test_network.py ├── test_storage.py └── test_translate.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [{*.py,setup.cfg}] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | workflow_dispatch: # Enables manual trigger 5 | pull_request: 6 | push: 7 | 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: "3.12" 21 | 22 | - name: Install Poetry 23 | run: | 24 | curl -sSL https://install.python-poetry.org | python3 - 25 | echo "$HOME/.local/bin" >> $GITHUB_PATH 26 | 27 | - name: List files in root 28 | run: ls -la 29 | 30 | - name: Print working directory 31 | run: pwd 32 | 33 | - name: Check readme permissions 34 | run: ls -l readme.md 35 | 36 | - name: Install dependencies 37 | run: poetry install --with dev --verbose 38 | 39 | - name: Ruff Linting and Formatting Check 40 | run: poetry run ruff check ./src/anonymizer/ --fix 41 | 42 | - name: Run Unit Tests 43 | env: 44 | PYTHONPATH: ${{ github.workspace }}/src 45 | AWS_USERNAME: ${{ secrets.AWS_USERNAME }} 46 | AWS_PASSWORD: ${{ secrets.AWS_PASSWORD }} 47 | run: | 48 | 49 | # Run tests with text coverage report 50 | poetry run pytest 51 | 52 | # > test_coverage_output.txt 53 | 54 | # # Print the contents of test_coverage_output.txt 55 | # echo "Test & Coverage Report:" 56 | # cat test_coverage_output.txt 57 | 58 | # # Get the coverage percentage from the output 59 | # coverage_percentage=$(grep "TOTAL" test_coverage_output.txt | awk '{print $4}' | tr -d '%') 60 | 61 | # threshold=10 62 | 63 | # # Check if coverage meets the threshold 64 | # if (( coverage_percentage >= threshold )); then 65 | # echo "Coverage is above $threshold%: $coverage_percentage%" 66 | # exit 0 # Exit with success 67 | # else 68 | # echo "Coverage is below $threshold%: $coverage_percentage%. Tests failed." 69 | # exit 1 # Exit with failure 70 | # fi 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dcm 2 | *.pth 3 | *.dmg 4 | *.log 5 | *.tmp 6 | *.DS_Store 7 | *.pem 8 | __pycache__/ 9 | *.py[cod] 10 | *.log 11 | *.env 12 | *.spec 13 | *.zip 14 | *.coverage 15 | *.anonymizer_state.json 16 | ProjectModel.json 17 | htmlcov/* 18 | dist/* 19 | build/* 20 | src/build/* 21 | src/dist/* 22 | src/logs/* 23 | src/certs/* 24 | src/assets/ocr/* 25 | dicom/* 26 | phi_imgs/* 27 | **/versionfile.txt 28 | **/radon_results.txt 29 | Pipfile.lock 30 | .python-version -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name":"Python Debugger: Current File", 6 | "type":"debugpy", 7 | "request":"launch", 8 | "program":"${file}", 9 | "console":"integratedTerminal", 10 | }, 11 | { 12 | "name": "Anonymizer GUI", 13 | "type": "debugpy", 14 | "request": "launch", 15 | "module": "anonymizer.anonymizer", 16 | "cwd": "${workspaceFolder}/src", 17 | "env": { 18 | "PYTHONOPTIMIZE": "0", 19 | }, 20 | "console": "integratedTerminal", 21 | "autoReload": { 22 | "enable": true 23 | }, 24 | "justMyCode": false, 25 | }, 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.editor.labelFormat": "medium", // short,medium,long 3 | "workbench.editor.showTabs": "multiple", 4 | "python.envFile": "${workspaceFolder}/.env", 5 | "python.terminal.activateEnvironment": true, 6 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", 7 | "python.experiments.optOutFrom": ["pythonTestAdapter"], 8 | "python.testing.unittestEnabled": false, 9 | "python.testing.pytestEnabled": true, 10 | "[python]": { 11 | "editor.defaultFormatter": "charliermarsh.ruff", 12 | "editor.formatOnSave": true, 13 | }, 14 | "python.languageServer": "Pylance", 15 | "python.analysis.typeCheckingMode": "basic", 16 | 17 | } -------------------------------------------------------------------------------- /docs/3_managing anonymization projects.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RSNA DICOM Anonymizer Managing Projects 7 | 8 | 9 | 10 | 11 |

Managing Anonymization Projects

12 | 13 |

Creating a New Project

14 | 15 | 19 | 20 |

Closing a Project

21 | 22 | 25 | 26 |

Re-opening a Project

27 | 28 | 33 | 34 |

Cloning a Project

35 | 36 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/6_license.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

License

5 | The RSNA DICOM Anonymizer software is released under the 6 | RSNA Public License. 7 |

8 |

Dependencies

9 |
    10 |
  1. CustomTkinter
  2. 11 |
  3. Python Imaging Library (PIL): Pillow
  4. 12 |
  5. tkhtmlview
  6. 13 |
  7. pydicom
  8. 14 |
  9. pynetdicom
  10. 15 |
  11. ifaddr
  12. 16 |
  13. boto3
  14. 17 |
  15. openpyxl
  16. 18 |
  17. psutil
  18. 19 |
  19. pylibjpeg
  20. 20 |
  21. numpy
  22. 21 |
  23. opencv-python-headless
  24. 22 |
  25. easyocr
  26. 23 |
  27. cryptography
  28. 24 |
  29. requests
  30. 25 |
  31. requests-oauthlib
  32. 26 |
  33. toml
  34. 27 |
  35. click
  36. 28 |
  37. dataclasses-json
  38. 29 |
30 |

Dependencies open source licenses

31 | The software above, included in the RSNA DICOM Anonymizer, is released under the following licenses: 32 | 38 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/images/AWSCognitoCredentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/AWSCognitoCredentials.png -------------------------------------------------------------------------------- /docs/images/CloneProject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/CloneProject.png -------------------------------------------------------------------------------- /docs/images/CloseProject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/CloseProject.png -------------------------------------------------------------------------------- /docs/images/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/Dashboard.png -------------------------------------------------------------------------------- /docs/images/ExportServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/ExportServer.png -------------------------------------------------------------------------------- /docs/images/ExportStudiesAWS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/ExportStudiesAWS.png -------------------------------------------------------------------------------- /docs/images/ExportStudiesStatus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/ExportStudiesStatus.png -------------------------------------------------------------------------------- /docs/images/ImportFilesDialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/ImportFilesDialog.png -------------------------------------------------------------------------------- /docs/images/ImportFilesMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/ImportFilesMenu.png -------------------------------------------------------------------------------- /docs/images/ImportStudiesDialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/ImportStudiesDialog.png -------------------------------------------------------------------------------- /docs/images/ImportStudiesResult.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/ImportStudiesResult.png -------------------------------------------------------------------------------- /docs/images/LocalServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/LocalServer.png -------------------------------------------------------------------------------- /docs/images/LoggingLevels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/LoggingLevels.png -------------------------------------------------------------------------------- /docs/images/Modalities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/Modalities.png -------------------------------------------------------------------------------- /docs/images/NetworkTimeouts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/NetworkTimeouts.png -------------------------------------------------------------------------------- /docs/images/NewProject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/NewProject.png -------------------------------------------------------------------------------- /docs/images/NewProjectSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/NewProjectSettings.png -------------------------------------------------------------------------------- /docs/images/OpenLogViewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/OpenLogViewer.png -------------------------------------------------------------------------------- /docs/images/OpenRecent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/OpenRecent.png -------------------------------------------------------------------------------- /docs/images/PatientLookupData.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/PatientLookupData.png -------------------------------------------------------------------------------- /docs/images/PatientLookupSave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/PatientLookupSave.png -------------------------------------------------------------------------------- /docs/images/QueryRetrieveImport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/QueryRetrieveImport.png -------------------------------------------------------------------------------- /docs/images/QueryServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/QueryServer.png -------------------------------------------------------------------------------- /docs/images/SelectDirectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/SelectDirectory.png -------------------------------------------------------------------------------- /docs/images/SelectFiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/SelectFiles.png -------------------------------------------------------------------------------- /docs/images/SelectQuery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/SelectQuery.png -------------------------------------------------------------------------------- /docs/images/StorageClasses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/StorageClasses.png -------------------------------------------------------------------------------- /docs/images/StorageDirectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/StorageDirectory.png -------------------------------------------------------------------------------- /docs/images/TransferSyntaxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/TransferSyntaxes.png -------------------------------------------------------------------------------- /docs/images/Welcome_en_osx_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/Welcome_en_osx_light.png -------------------------------------------------------------------------------- /docs/images/Welcome_en_win_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/images/Welcome_en_win_light.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RSNA DICOM Anonymizer Help 7 | 8 | 9 | 10 | 11 |
12 | Welcome Image 13 |
    14 |

    RSNA DICOM Anonymizer

    15 |
  1. Overview
  2. 16 |
  3. De-identification Protocol
  4. 17 |
  5. Managing Anonymization Projects
  6. 18 |
  7. Project Settings
  8. 19 |
  9. Operation
  10. 20 |
  11. License
  12. 21 |
22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/refs/AttributeLevelConfidentiality_sup55.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/refs/AttributeLevelConfidentiality_sup55.pdf -------------------------------------------------------------------------------- /docs/refs/CTP-update-notes_jperry.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/refs/CTP-update-notes_jperry.pdf -------------------------------------------------------------------------------- /docs/refs/Issues/c-move_vna.txt: -------------------------------------------------------------------------------- 1 | DICOM was not designed for Cloud/VNA (aka Wide Area Network) Servers 2 | 3 | The DICOM protocol was developed to target intranets to support medical imaging in a single hospital not the internet or cloud. 4 | 5 | 6 | D.3.3.3 Asynchronous Operations (And Sub-Operations) Window Negotiation 7 | https://dicom.nema.org/medical/dicom/current/output/chtml/part07/sect_D.3.3.3.html 8 | 9 | To C-MOVE is human; to C-GET, divine 10 | https://dclunie.blogspot.com/2016/05/to-c-move-is-human-to-c-get-divine.html 11 | 12 | Understanding DICOM with Orthanc: Using HTTP instead of the DICOM protocol 13 | https://book.orthanc-server.com/dicom-guide.html?highlight=file%20system#using-http-instead-of-the-dicom-protocol 14 | 15 | -------------------------------------------------------------------------------- /docs/refs/ModalityForReportsRequestDocs_cp749_lb.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/refs/ModalityForReportsRequestDocs_cp749_lb.pdf -------------------------------------------------------------------------------- /docs/refs/RSNA-Covid-19-Deidentification-Protocol.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/docs/refs/RSNA-Covid-19-Deidentification-Protocol.pdf -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f0f0f0; 3 | color: #014F8F; 4 | font-family:Tahoma; 5 | font-size: 1.25em; 6 | .image-list-container { 7 | display: flex; 8 | align-items: center; 9 | } 10 | .image-list-container img { 11 | margin-right: 10px; 12 | } 13 | } -------------------------------------------------------------------------------- /patch_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | # Get the next minor version number from poetry 4 | VERSION=$(poetry version minor | awk '{print $NF}') 5 | 6 | echo "Releasing Version: $VERSION" 7 | 8 | # Tag the release in git 9 | git tag "$VERSION" 10 | 11 | # Push the tags to the origin remote 12 | git push origin --tags 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "rsna-anonymizer" 3 | version = "17.4.6" 4 | description = "RSNA DICOM Anonymizer" 5 | authors = ["Algorhythm Software "] 6 | license = "RSNA-MIRC Public License" 7 | readme = "readme.md" 8 | homepage = "https://github.com/RSNA/anonymizer" 9 | packages = [{ include = "anonymizer", from = "src"}] 10 | 11 | [tool.poetry.dependencies] 12 | python = ">=3.12.0,<3.13.0" 13 | numpy = "<2.0.0" 14 | customtkinter = "*" 15 | pillow = "*" 16 | tkhtmlview = "*" 17 | pydicom = ">=2.4,<2.5" 18 | pynetdicom = "*" 19 | ifaddr = "*" 20 | boto3 = "*" 21 | openpyxl = "*" 22 | psutil = "*" 23 | pylibjpeg = {extras = ["all"], version = "*"} 24 | opencv-python-headless = "*" 25 | easyocr = "*" 26 | cryptography = "*" 27 | requests = "*" 28 | requests-oauthlib = "*" 29 | toml = "^0.10.2" 30 | click = "^8.1.8" 31 | dataclasses-json = "^0.6.7" 32 | bidict = "^0.23.1" 33 | 34 | [tool.poetry.group.dev.dependencies] 35 | poetry = "^2.0.1" 36 | ruff = "*" 37 | black = "*" 38 | pytest = "*" 39 | pytest-cov = "^6.0.0" 40 | python-dotenv = "*" 41 | ffmpeg-python = "^0.2.0" 42 | pytest-mock = "^3.14.1" 43 | 44 | [tool.poetry.scripts] 45 | rsna-anonymizer = "anonymizer.anonymizer:main" 46 | 47 | [tool.ruff] 48 | target-version = "py312" 49 | line-length = 120 50 | 51 | lint.select = [ # https://docs.astral.sh/ruff/linter/#rule-selection 52 | "E", # PyCodeStyle 53 | "F", # PyFlakes 54 | "W", # PyCodeStyle 55 | "I", # iSort 56 | "B", # flake8-bugbear 57 | "SIM", # flake8-simplify 58 | "C90", # mccabe 59 | ] 60 | 61 | lint.fixable = [ #All the errors that are fixed with --fix, SAFE only 62 | "I001", # Import block is un-sorted or un-formatted 63 | "W292", # No newline at end of file 64 | "W293", # Blank line contains whitespace 65 | "W291", # Trailing whitespace 66 | "F401", # Imported but unused 67 | ] 68 | 69 | lint.ignore = [ 70 | "E501", # Line too long 71 | "SIM102", # Nested if statements 72 | "C901", # Mccabe complexity 73 | ] 74 | 75 | [tool.ruff.lint.mccabe] 76 | max-complexity = 15 # Default is 10 77 | 78 | [tool.pytest.ini_options] 79 | testpaths = ["tests"] 80 | # addopts = "--verbose --show-capture=all --cov=src/anonymizer --cov-report=term-missing" 81 | addopts = "--verbose --cov=src/anonymizer/controller --cov=src/anonymizer/utils --cov=src/anonymizer/model" 82 | 83 | [build-system] 84 | requires = ["poetry-core>=2.0.0,<3.0.0"] 85 | build-backend = "poetry.core.masonry.api" 86 | 87 | 88 | -------------------------------------------------------------------------------- /readme.de.md: -------------------------------------------------------------------------------- 1 | # RSNA DICOM Anonymisierer V17.3 2 | [![en](https://img.shields.io/badge/lang-en-blue.svg)](readme.md) 3 | [![es](https://img.shields.io/badge/lang-es-blue.svg)](readme.es.md) 4 | [![fr](https://img.shields.io/badge/lang-fr-blue.svg)](readme.fr.md) 5 | [![Tests](https://github.com/RSNA/anonymizer/actions/workflows/tests.yaml/badge.svg)](https://github.com/RSNA/anonymizer/actions/workflows/tests.yaml) 6 | ## Python mit tkinter (GUI-Bibliothek) installieren 7 | ### Windows 8 | 1. Laden Sie Python 3.12+ von [python.org](https://www.python.org/downloads/) herunter 9 | 2. Führen Sie das Installationsprogramm aus 10 | - Wählen Sie "Add python.exe to PATH" 11 | - Aktivieren Sie "tcl/tk und IDLE" 12 | ### macOS 13 | 1. Installieren Sie Homebrew, falls nicht vorhanden: `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)` 14 | 2. Installieren Sie Python 3.12 mit Tcl/Tk: 15 | ``` 16 | brew install python@3.12 17 | brew install tcl-tk 18 | ``` 19 | ### Linux (Ubuntu/Debian) 20 | 1. Installieren Sie die erforderlichen Pakete: 21 | ``` 22 | sudo apt update 23 | sudo apt install software-properties-common 24 | sudo add-apt-repository ppa:deadsnakes/ppa 25 | sudo apt install python3.12 python3.12-tk 26 | ``` 27 | ## Installation überprüfen 28 | ``` 29 | python --version 30 | python -m tkinter 31 | ``` 32 | Wenn Python + tkinter erfolgreich installiert wurde, sollte ein kleines GUI-Fenster geöffnet werden 33 | ## rsna-anonymizer Paket von PyPI installieren 34 | `pip install rsna-anonymizer` 35 | ## Ausführung 36 | `rsna-anonymizer` 37 | ## Aktualisierung 38 | `pip install --upgrade rsna-anonymizer` 39 | ## Dokumentation 40 | [Hilfedateien](https://mdevans.github.io/anonymizer/index.html) 41 | -------------------------------------------------------------------------------- /readme.es.md: -------------------------------------------------------------------------------- 1 | # RSNA DICOM Anonimizador V17.3 2 | [![en](https://img.shields.io/badge/lang-en-blue.svg)](readme.md) 3 | [![de](https://img.shields.io/badge/lang-de-blue.svg)](readme.de.md) 4 | [![fr](https://img.shields.io/badge/lang-fr-blue.svg)](readme.fr.md) 5 | [![Tests](https://github.com/RSNA/anonymizer/actions/workflows/tests.yaml/badge.svg)](https://github.com/RSNA/anonymizer/actions/workflows/tests.yaml) 6 | 7 | ## Instalar Python con tkinter (biblioteca GUI) 8 | ### Windows 9 | 1. Descarga Python 3.12+ desde [python.org](https://www.python.org/downloads/) 10 | 2. Ejecuta el instalador 11 | - Selecciona "Add python.exe to PATH" 12 | - Habilita "tcl/tk and IDLE" 13 | ### macOS 14 | 1. Instala Homebrew si no está presente: `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)' 15 | 2. Instala Python 3.12 con Tcl/Tk: 16 | ``` 17 | brew install python@3.12 18 | brew install tcl-tk 19 | ``` 20 | ### Linux (Ubuntu/Debian) 21 | 1. Instala los paquetes requeridos: 22 | ``` 23 | sudo apt update 24 | sudo apt install software-properties-common 25 | sudo add-apt-repository ppa:deadsnakes/ppa 26 | sudo apt install python3.12 python3.12-tk 27 | ``` 28 | ## Verificar Instalación 29 | ``` 30 | python --version 31 | python -m tkinter 32 | ``` 33 | Si python + tkinter se han instalado correctamente, se abrirá una pequeña ventana GUI 34 | ## Instalar el paquete rsna-anonymizer desde PyPI 35 | `pip install rsna-anonymizer` 36 | ## Ejecución 37 | `rsna-anonymizer` 38 | ## Actualización 39 | `pip install --upgrade rsna-anonymizer` 40 | ## Documentación 41 | [Archivos de ayuda](https://mdevans.github.io/anonymizer/index.html) 42 | -------------------------------------------------------------------------------- /readme.fr.md: -------------------------------------------------------------------------------- 1 | # RSNA DICOM l'Anonymiseur V17 2 | [![en](https://img.shields.io/badge/lang-en-blue.svg)](readme.md) 3 | [![de](https://img.shields.io/badge/lang-de-blue.svg)](readme.de.md) 4 | [![es](https://img.shields.io/badge/lang-es-blue.svg)](readme.es.md) 5 | [![Tests](https://github.com/RSNA/anonymizer/actions/workflows/tests.yaml/badge.svg)](https://github.com/RSNA/anonymizer/actions/workflows/tests.yaml) 6 | ## Installation de Python avec tkinter (bibliothèque GUI) 7 | ### Windows 8 | 1. Téléchargez Python 3.12+ depuis [python.org](https://www.python.org/downloads/) 9 | 2. Exécutez l'installateur 10 | - Sélectionnez "Add python.exe to PATH" 11 | - Activez "tcl/tk and IDLE" 12 | ### macOS 13 | 1. Installez Homebrew si absent : `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` 14 | 2. Installez Python 3.12 avec Tcl/Tk : 15 | ``` 16 | brew install python@3.12 17 | brew install tcl-tk 18 | ``` 19 | ### Linux (Ubuntu/Debian) 20 | 1. Installez les paquets requis : 21 | ``` 22 | sudo apt update 23 | sudo apt install software-properties-common 24 | sudo add-apt-repository ppa:deadsnakes/ppa 25 | sudo apt install python3.12 python3.12-tk 26 | ``` 27 | ## Vérification de l'installation 28 | ``` 29 | python --version 30 | python -m tkinter 31 | ``` 32 | Si python + tkinter sont installés correctement, une petite fenêtre GUI devrait s'ouvrir 33 | ## Installation du paquet rsna-anonymizer depuis PyPI 34 | `pip install rsna-anonymizer` 35 | ## Exécution 36 | `rsna-anonymizer` 37 | ## Mise à jour 38 | `pip install --upgrade rsna-anonymizer` 39 | ## Documentation 40 | [Fichiers d'aide](https://mdevans.github.io/anonymizer/index.html) 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # RSNA DICOM Anonymizer V17.4 2 | [![de](https://img.shields.io/badge/lang-de-blue.svg)](readme.de.md) 3 | [![es](https://img.shields.io/badge/lang-es-blue.svg)](readme.es.md) 4 | [![fr](https://img.shields.io/badge/lang-fr-blue.svg)](readme.fr.md) 5 | [![Tests](https://github.com/RSNA/anonymizer/actions/workflows/tests.yaml/badge.svg)](https://github.com/RSNA/anonymizer/actions/workflows/tests.yaml) 6 | 7 | ## Install Python with tkinter (GUI library) 8 | ### Windows 9 | 1. Download Python 3.12 from [python.org](https://www.python.org/downloads/) 10 | 2. Run installer 11 | - Select "Add python.exe to PATH" 12 | - Enable "tcl/tk and IDLE" 13 | ### macOS 14 | 1. Install Homebrew if not present: `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)' 15 | 2. Install Python 3.12 with Tcl/Tk: 16 | ``` 17 | brew install python@3.12 18 | brew install tcl-tk 19 | ``` 20 | ### Linux (Ubuntu/Debian) 21 | 1. Install the required packages: 22 | ``` 23 | sudo apt update 24 | sudo apt install software-properties-common 25 | sudo add-apt-repository ppa:deadsnakes/ppa 26 | sudo apt install python3.12 python3.12-tk 27 | ``` 28 | ## Verify Installation 29 | ``` 30 | python --version 31 | python -m tkinter 32 | ``` 33 | If python + tkinter has been installed successfully a small GUI window should open 34 | ## Install rsna-anonymizer package from PyPI 35 | `pip install rsna-anonymizer` 36 | ## Execution 37 | `rsna-anonymizer` 38 | ## Upgrading 39 | `pip install --upgrade rsna-anonymizer` 40 | ## Documentation 41 | [Help files](https://rsna.github.io/anonymizer) 42 | ## Development 43 | ### Setup 44 | 1. Setup python environment (>3.10) which includes Tkinter, recommend using pyenv with MacOS & Linux 45 | 2. Ensure python is installed with Tkinter: `python -m tkinter`, a small GUI window should open 46 | 3. Install poetry: `pip install poetry` 47 | 4. Set virtual environment within project: `poetry config virtualenvs.in-project true` 48 | 4. Clone repository 49 | 5. Setup virtual environment and install all dependencies listed in pyproject.toml: `poetry install --with dev` 50 | ### Unit Testing 51 | #### For model and controller with coverage 52 | ``` 53 | 1. Create tests/controller/.env file with your AWS_USERNAME and AWS_PASSWORD 54 | 2. poetry run pytest 55 | ``` 56 | ### Translations 57 | Languages for 17.3: `en_US, de, es, fr` 58 | #### Ensure gettext is installed: 59 | 1. Windows: [Install instructions](https://mlocati.github.io/articles/gettext-iconv-windows.html) or `choco install gettext` 60 | 2. Mac OSX: `brew install gettext` 61 | 3. Linux: `sudo apt-get install gettext` 62 | #### Extracting messages from source files: 63 | cd src/anonymizer/assets/locales/ 64 | ./extract_translations.sh 65 | #### Updating translations: 66 | cd src/anonymizer/assets/locales/ 67 | ./update_translations.sh 68 | ### Software Architecture 69 | Full class diagram on github [here](class_diagram.md) 70 | -------------------------------------------------------------------------------- /src/anonymizer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/__init__.py -------------------------------------------------------------------------------- /src/anonymizer/assets/fonts/DINAlternate-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/fonts/DINAlternate-Bold.ttf -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/create_icns.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | test -n "$1" 5 | name="${1%.*}" 6 | 7 | iconset="${name}.iconset" 8 | rm -rf "${iconset}" 9 | mkdir -p "${iconset}" 10 | 11 | for s in 16 32 128 256 512; do 12 | d=$(($s*2)) 13 | sips -Z $s "$1" --out "${iconset}/icon_${s}x$s.png" 14 | sips -Z $d "$1" --out "${iconset}/icon_${s}x$s@2x.png" 15 | done 16 | 17 | iconutil -c icns "${iconset}" -o "${name}.icns" 18 | rm -r "${iconset}" -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/kaleidoscope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/kaleidoscope.png -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/kaleidoscope_flat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/kaleidoscope_stack_angle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/kaleidoscope_stack_vert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/pause.png -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/play.png -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/rsna_icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/rsna_icon.icns -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/rsna_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/rsna_icon.ico -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/rsna_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/rsna_icon.png -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/rsna_logo_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/rsna_logo_alpha.png -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/rsna_logo_head_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/rsna_logo_head_profile.png -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/rsna_logo_head_profile_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/rsna_logo_head_profile_1024x1024.png -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/rsna_logo_head_profile_titled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/rsna_logo_head_profile_titled.png -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/rsna_titled_logo_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/icons/rsna_titled_logo_alpha.png -------------------------------------------------------------------------------- /src/anonymizer/assets/icons/signpath_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 17 | 20 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | cryptography = "*" 10 | 11 | [requires] 12 | python_version = "3.11" 13 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/3_anonymisierungsprojekte_verwalten.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 |
7 | 8 |

Verwaltung von Anonymisierungsprojekten

9 | 10 |

Ein neues Projekt erstellen

11 | 12 | 16 | 17 |

Schließen eines Projekts

18 | 19 | 22 | 23 |

Wiedereröffnen eines Projekts

24 | 25 | 30 | 31 |

Klonen eines Projekts

32 | 33 | 41 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/6_lizenz.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Lizenz

5 | Die RSNA DICOM Anonymizer-Software wird unter der 6 | RSNA Public License veröffentlicht. 7 |

8 |

Abhängigkeiten

9 |
    10 |
  1. CustomTkinter
  2. 11 |
  3. Python Imaging Library (PIL): Pillow
  4. 12 |
  5. tkhtmlview
  6. 13 |
  7. pydicom
  8. 14 |
  9. pynetdicom
  10. 15 |
  11. ifaddr
  12. 16 |
  13. boto3
  14. 17 |
  15. pywin32-ctypes
  16. 18 |
  17. pefile
  18. 19 |
  19. openpyxl
  20. 20 |
21 |

Open-Source-Lizenzen der Abhängigkeiten

22 | Die oben genannte Software, die im RSNA DICOM Anonymizer enthalten ist, wird unter den folgenden Lizenzen veröffentlicht: 23 | 29 | 30 |
31 | 32 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/AWSCognitoCredentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/AWSCognitoCredentials.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/AbfrageAbrufenImportieren.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/AbfrageAbrufenImportieren.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Auswahlabfrage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Auswahlabfrage.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/BildarchivQRServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/BildarchivQRServer.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Dashboard.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Dateiauswahloptionen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Dateiauswahloptionen.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/DateienImportieren.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/DateienImportieren.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/ExportServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/ExportServer.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/ExportStudienAWS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/ExportStudienAWS.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/ExportStudienStatus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/ExportStudienStatus.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/ImportlokalenDateisystem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/ImportlokalenDateisystem.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/KlonenProjekts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/KlonenProjekts.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/LokalerServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/LokalerServer.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Modalitäten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Modalitäten.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Netzwerk-Zeitüberschreitungen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Netzwerk-Zeitüberschreitungen.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/NeueProjekteinstellungen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/NeueProjekteinstellungen.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/NeuesProjekt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/NeuesProjekt.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Patienten-Such-CSV-Datei.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Patienten-Such-CSV-Datei.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/PatientenPHI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/PatientenPHI.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Projektschließen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Projektschließen.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Protokollierungsstufen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Protokollierungsstufen.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Speicherklassen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Speicherklassen.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Speicherverzeichnis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Speicherverzeichnis.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/TransferSyntaxen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/TransferSyntaxen.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/VerzeichnisAuswählen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/VerzeichnisAuswählen.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Welcome_de_osx_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Welcome_de_osx_light.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/Welcome_de_win_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/Welcome_de_win_light.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/html/images/WiedereröffnenProjekts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/de/html/images/WiedereröffnenProjekts.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/whitelists/ct.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on CT images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | 14 | # --- Orientation / Position --- 15 | AX 16 | AXIAL 17 | COR 18 | CORONAL 19 | SAG 20 | SAGITTAL 21 | OBLIQUE 22 | SUPINE 23 | PRONE 24 | DECUBITUS 25 | ERECT 26 | SEMI-ERECT 27 | HEAD FIRST 28 | FEET FIRST 29 | HF 30 | FF 31 | HFS 32 | FFS 33 | HFP 34 | FFP 35 | 36 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 37 | HEAD 38 | NECK 39 | CHEST 40 | ABDOMEN 41 | PELVIS 42 | SPINE 43 | EXTREMITY 44 | BRAIN 45 | LUNG 46 | LIVER 47 | KIDNEY 48 | HEART 49 | AORTA 50 | VESSEL 51 | 52 | # --- Scanner / Technical Parameters --- 53 | CT 54 | SCAN 55 | SCOUT 56 | TOPOGRAM 57 | SURVIEW 58 | SCANOGRAM 59 | HELICAL 60 | SEQUENTIAL 61 | VOLUME 62 | ACQ 63 | RECON 64 | SLICE 65 | THICKNESS 66 | SL 67 | THK 68 | FOV 69 | DFOV 70 | ZOOM 71 | WW 72 | WL 73 | WINDOW 74 | LEVEL 75 | WIDTH 76 | KV 77 | KVP 78 | MA 79 | MAS 80 | TIME 81 | ROT TIME 82 | ROTATION 83 | PITCH 84 | NOISE 85 | INDEX 86 | CTDI 87 | CTDI VOL 88 | DLP 89 | KERNEL 90 | FILTER 91 | STANDARD 92 | SOFT 93 | BONE 94 | LUNG 95 | EDGE 96 | SHARP 97 | SMOOTH 98 | ITERATIVE 99 | IR 100 | ASIR 101 | MBIR 102 | IMR 103 | EXPOSURE 104 | 105 | # --- Contrast / Timing --- 106 | CONTRAST 107 | CONT 108 | WITH CONTRAST 109 | W CONTRAST 110 | W/C 111 | WITHOUT CONTRAST 112 | WO CONTRAST 113 | NON CON 114 | NON-CON 115 | PRE 116 | POST 117 | PRE CONTRAST 118 | POST CONTRAST 119 | ARTERIAL 120 | ART 121 | VENOUS 122 | VEN 123 | DELAY 124 | DELAYED 125 | NEPHROGRAPHIC 126 | EXCRETORY 127 | PORTAL 128 | PV 129 | ORAL 130 | IV 131 | BOLUS 132 | INJECTION 133 | 134 | # --- Measurements / Units --- 135 | MM 136 | CM 137 | HU # Hounsfield Unit 138 | SUV # Standardized Uptake Value (PET/CT) 139 | 140 | # --- Miscellaneous --- 141 | SERIES 142 | IMAGE 143 | IMG 144 | NO 145 | NUM 146 | NUMBER 147 | SCAN DATE 148 | SCAN TIME 149 | ACQ DATE 150 | ACQ TIME 151 | TABLE 152 | HEIGHT 153 | POS 154 | POSITION 155 | REF 156 | REFERENCE 157 | NONE 158 | N/A 159 | VARIOUS 160 | SEE REPORT 161 | CLINICAL HISTORY 162 | PROTOCOL 163 | AUTO 164 | 165 | # --- Common Artifacts / Descriptions (Use cautiously) --- 166 | MOTION 167 | ARTIFACT 168 | METAL 169 | STREAKING 170 | BEAM HARDENING 171 | 172 | # --- Add institution-specific, non-PHI identifiers if necessary --- 173 | # E.g., SCANNER01, RAD_ROOM_A, PROTOCOL_ABC 174 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/whitelists/dx.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on DX/CR (X-Ray) images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | BILAT 14 | 15 | # --- Orientation / Position --- 16 | AP # Anteroposterior 17 | PA # Posteroanterior 18 | LAT # Lateral 19 | OBL # Oblique 20 | DECUB # Decubitus 21 | SUPINE 22 | PRONE 23 | ERECT 24 | SEMI-ERECT 25 | STANDING 26 | SITTING 27 | RECUMBENT 28 | WEIGHT BEARING 29 | WB 30 | NON WEIGHT BEARING 31 | NWB 32 | PORTABLE 33 | PORT 34 | BEDSIDE 35 | MOBILE 36 | INSPIRATION 37 | EXPIRATION 38 | FLEXION 39 | EXTENSION 40 | INTERNAL ROTATION 41 | EXTERNAL ROTATION 42 | AXIAL 43 | CEPHALAD 44 | CAUDAD 45 | 46 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 47 | HEAD 48 | SKULL 49 | NECK 50 | CHEST 51 | CXR # Chest X-Ray 52 | KUB # Kidneys, Ureters, Bladder 53 | ABDOMEN 54 | PELVIS 55 | SPINE 56 | CERVICAL 57 | THORACIC 58 | LUMBAR 59 | SACRUM 60 | COCCYX 61 | SHOULDER 62 | ELBOW 63 | WRIST 64 | HAND 65 | HIP 66 | KNEE 67 | ANKLE 68 | FOOT 69 | EXTREMITY 70 | UPPER 71 | LOWER 72 | RIBS 73 | 74 | # --- Technical Parameters --- 75 | DX 76 | CR 77 | DR 78 | XRAY 79 | X-RAY 80 | PORTABLE 81 | GRID 82 | NO GRID 83 | AEC # Automatic Exposure Control 84 | MANUAL 85 | KV 86 | KVP 87 | MA 88 | MAS 89 | EXPOSURE 90 | TIME 91 | SID # Source-to-Image Distance 92 | FFD # Film-Focus Distance 93 | MAG # Magnification 94 | TECH 95 | TECHNOLOGIST 96 | SCOUT 97 | 98 | # --- Miscellaneous --- 99 | SERIES 100 | IMAGE 101 | IMG 102 | VIEW 103 | PROJECTION 104 | NO 105 | NUM 106 | NUMBER 107 | EXAM DATE 108 | EXAM TIME 109 | TABLE 110 | HEIGHT 111 | POS 112 | POSITION 113 | REF 114 | REFERENCE 115 | NONE 116 | N/A 117 | VARIOUS 118 | SEE REPORT 119 | CLINICAL HISTORY 120 | COMPARISON 121 | PREVIOUS 122 | PRIOR 123 | 124 | # --- Common Artifacts / Descriptions (Use cautiously) --- 125 | MOTION 126 | ARTIFACT 127 | BLUR 128 | CLOTHING 129 | JEWELRY 130 | FOREIGN BODY 131 | IMPLANT 132 | PACEMAKER 133 | LINE 134 | TUBE 135 | 136 | # --- Add institution-specific, non-PHI identifiers if necessary --- 137 | # E.g., XRAY_ROOM_1, PORTABLE_UNIT_3 138 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/de/whitelists/us.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on Ultrasound (US) images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | BILAT 14 | 15 | # --- Orientation / Position --- 16 | SAG # Sagittal 17 | TRV # Transverse 18 | TRA # Transverse 19 | COR # Coronal 20 | LONG # Longitudinal 21 | AXIAL 22 | AX 23 | OBLIQUE 24 | SUPINE 25 | PRONE 26 | DECUBITUS 27 | LLD # Left Lateral Decubitus 28 | RLD # Right Lateral Decubitus 29 | ERECT 30 | SEMI-ERECT 31 | SITTING 32 | 33 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 34 | HEAD 35 | NECK 36 | THYROID 37 | CAROTID 38 | CHEST 39 | BREAST 40 | ABDOMEN 41 | LIVER 42 | GALLBLADDER 43 | GB 44 | CBD # Common Bile Duct 45 | PANCREAS 46 | SPLEEN 47 | KIDNEY 48 | RENAL 49 | AORTA 50 | IVC 51 | PELVIS 52 | UTERUS 53 | OVARY 54 | PROSTATE 55 | TESTIS 56 | SCROTUM 57 | BLADDER 58 | EXTREMITY 59 | ARM 60 | LEG 61 | VENOUS 62 | ARTERIAL 63 | DVT # Deep Vein Thrombosis 64 | APPENDIX 65 | HEART # Echocardiography terms often differ significantly 66 | FETAL 67 | OB # Obstetrics 68 | GYN # Gynecology 69 | 70 | # --- Scanner / Technical Parameters --- 71 | US 72 | ULTRA SOUND 73 | SONO 74 | SONOGRAM 75 | PROBE 76 | TRANSDUCER 77 | LINEAR 78 | CURVED 79 | SECTOR 80 | ENDO # Endocavitary (e.g., Endovaginal, Endorectal) 81 | EV # Endovaginal 82 | ER # Endorectal 83 | FREQ # Frequency 84 | MHZ # Megahertz 85 | GAIN 86 | TGC # Time Gain Compensation 87 | DEPTH 88 | FOCUS 89 | FOV 90 | ZOOM 91 | HARMONIC 92 | THI # Tissue Harmonic Imaging 93 | COMPOUND 94 | POWER 95 | DOPPLER 96 | COLOR 97 | CD # Color Doppler 98 | PWR # Power Doppler 99 | PW # Pulsed Wave Doppler 100 | CW # Continuous Wave Doppler 101 | SPECTRAL 102 | VEL # Velocity 103 | PRF # Pulse Repetition Frequency 104 | FILTER 105 | SCALE 106 | ANGLE 107 | CURSOR 108 | CALIPER 109 | MEASURE 110 | DIST # Distance 111 | AREA 112 | VOLUME 113 | VOL 114 | MI # Mechanical Index 115 | TIS # Thermal Index Soft Tissue 116 | TIB # Thermal Index Bone 117 | TIC # Thermal Index Cranial Bone 118 | FR # Frame Rate 119 | FPS # Frames Per Second 120 | GRAYSCALE 121 | B MODE 122 | M MODE 123 | 124 | # --- Measurements / Units --- 125 | MM 126 | CM 127 | M/S # Meters per second 128 | CM/S # Centimeters per second 129 | KHZ # Kilohertz 130 | HZ # Hertz 131 | DEG # Degrees 132 | 133 | # --- Miscellaneous --- 134 | SERIES 135 | IMAGE 136 | IMG 137 | CINE 138 | LOOP 139 | CLIP 140 | VIEW 141 | PLANE 142 | SCAN 143 | NO 144 | NUM 145 | NUMBER 146 | EXAM DATE 147 | EXAM TIME 148 | ACQ DATE 149 | ACQ TIME 150 | POS 151 | POSITION 152 | REF 153 | REFERENCE 154 | NONE 155 | N/A 156 | VARIOUS 157 | SEE REPORT 158 | CLINICAL HISTORY 159 | PROTOCOL 160 | AUTO 161 | FREEZE 162 | PRINT 163 | STORE 164 | 165 | # --- Common Artifacts / Descriptions (Use cautiously) --- 166 | SHADOWING 167 | ENHANCEMENT 168 | REVERBERATION 169 | MIRROR 170 | ARTIFACT 171 | 172 | # --- Add institution-specific, non-PHI identifiers if necessary --- 173 | # E.g., US_ROOM_3, SONOGRAPHER_ID_XYZ 174 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/3_managing anonymization projects.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 |
7 | 8 |

Managing Anonymization Projects

9 | 10 |

Creating a New Project

11 | 12 | 16 | 17 |

Closing a Project

18 | 19 | 22 | 23 |

Re-opening a Project

24 | 25 | 30 | 31 |

Cloning a Project

32 | 33 | 42 | 43 |
44 | 45 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/6_license.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

License

5 | The RSNA DICOM Anonymizer software is released under the 6 | RSNA Public License. 7 |

8 |

Dependencies

9 |
    10 |
  1. CustomTkinter
  2. 11 |
  3. Python Imaging Library (PIL): Pillow
  4. 12 |
  5. tkhtmlview
  6. 13 |
  7. pydicom
  8. 14 |
  9. pynetdicom
  10. 15 |
  11. ifaddr
  12. 16 |
  13. boto3
  14. 17 |
  15. openpyxl
  16. 18 |
  17. psutil
  18. 19 |
  19. pylibjpeg
  20. 20 |
  21. numpy
  22. 21 |
  23. opencv-python-headless
  24. 22 |
  25. easyocr
  26. 23 |
  27. cryptography
  28. 24 |
  29. requests
  30. 25 |
  31. requests-oauthlib
  32. 26 |
  33. toml
  34. 27 |
  35. click
  36. 28 |
  37. dataclasses-json
  38. 29 |
30 |

Dependencies open source licenses

31 | The software above, included in the RSNA DICOM Anonymizer, is released under the following licenses: 32 | 38 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/AWSCognitoCredentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/AWSCognitoCredentials.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/CloneProject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/CloneProject.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/CloseProject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/CloseProject.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/Dashboard.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/ExportServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/ExportServer.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/ExportStudiesAWS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/ExportStudiesAWS.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/ExportStudiesStatus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/ExportStudiesStatus.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/ImportFilesDialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/ImportFilesDialog.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/ImportFilesMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/ImportFilesMenu.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/ImportStudiesDialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/ImportStudiesDialog.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/ImportStudiesResult.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/ImportStudiesResult.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/LocalServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/LocalServer.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/LoggingLevels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/LoggingLevels.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/Modalities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/Modalities.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/NetworkTimeouts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/NetworkTimeouts.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/NewProject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/NewProject.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/NewProjectSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/NewProjectSettings.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/OpenLogViewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/OpenLogViewer.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/OpenRecent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/OpenRecent.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/PatientLookupData.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/PatientLookupData.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/PatientLookupSave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/PatientLookupSave.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/QueryRetrieveImport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/QueryRetrieveImport.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/QueryServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/QueryServer.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/SelectDirectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/SelectDirectory.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/SelectFiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/SelectFiles.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/SelectQuery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/SelectQuery.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/StorageClasses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/StorageClasses.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/StorageDirectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/StorageDirectory.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/TransferSyntaxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/TransferSyntaxes.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/Welcome_en_osx_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/Welcome_en_osx_light.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/html/images/Welcome_en_win_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/en_US/html/images/Welcome_en_win_light.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/whitelists/ct.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on CT images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | 14 | # --- Orientation / Position --- 15 | AX 16 | AXIAL 17 | COR 18 | CORONAL 19 | SAG 20 | SAGITTAL 21 | OBLIQUE 22 | SUPINE 23 | PRONE 24 | DECUBITUS 25 | ERECT 26 | SEMI-ERECT 27 | HEAD FIRST 28 | FEET FIRST 29 | HF 30 | FF 31 | HFS 32 | FFS 33 | HFP 34 | FFP 35 | 36 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 37 | HEAD 38 | NECK 39 | CHEST 40 | ABDOMEN 41 | PELVIS 42 | SPINE 43 | EXTREMITY 44 | BRAIN 45 | LUNG 46 | LIVER 47 | KIDNEY 48 | HEART 49 | AORTA 50 | VESSEL 51 | 52 | # --- Scanner / Technical Parameters --- 53 | CT 54 | SCAN 55 | SCOUT 56 | TOPOGRAM 57 | SURVIEW 58 | SCANOGRAM 59 | HELICAL 60 | SEQUENTIAL 61 | VOLUME 62 | ACQ 63 | RECON 64 | SLICE 65 | THICKNESS 66 | SL 67 | THK 68 | FOV 69 | DFOV 70 | ZOOM 71 | WW 72 | WL 73 | WINDOW 74 | LEVEL 75 | WIDTH 76 | KV 77 | KVP 78 | MA 79 | MAS 80 | TIME 81 | ROT TIME 82 | ROTATION 83 | PITCH 84 | NOISE 85 | INDEX 86 | CTDI 87 | CTDI VOL 88 | DLP 89 | KERNEL 90 | FILTER 91 | STANDARD 92 | SOFT 93 | BONE 94 | LUNG 95 | EDGE 96 | SHARP 97 | SMOOTH 98 | ITERATIVE 99 | IR 100 | ASIR 101 | MBIR 102 | IMR 103 | EXPOSURE 104 | 105 | # --- Contrast / Timing --- 106 | CONTRAST 107 | CONT 108 | WITH CONTRAST 109 | W CONTRAST 110 | W/C 111 | WITHOUT CONTRAST 112 | WO CONTRAST 113 | NON CON 114 | NON-CON 115 | PRE 116 | POST 117 | PRE CONTRAST 118 | POST CONTRAST 119 | ARTERIAL 120 | ART 121 | VENOUS 122 | VEN 123 | DELAY 124 | DELAYED 125 | NEPHROGRAPHIC 126 | EXCRETORY 127 | PORTAL 128 | PV 129 | ORAL 130 | IV 131 | BOLUS 132 | INJECTION 133 | 134 | # --- Measurements / Units --- 135 | MM 136 | CM 137 | HU # Hounsfield Unit 138 | SUV # Standardized Uptake Value (PET/CT) 139 | 140 | # --- Miscellaneous --- 141 | SERIES 142 | IMAGE 143 | IMG 144 | NO 145 | NUM 146 | NUMBER 147 | SCAN DATE 148 | SCAN TIME 149 | ACQ DATE 150 | ACQ TIME 151 | TABLE 152 | HEIGHT 153 | POS 154 | POSITION 155 | REF 156 | REFERENCE 157 | NONE 158 | N/A 159 | VARIOUS 160 | SEE REPORT 161 | CLINICAL HISTORY 162 | PROTOCOL 163 | AUTO 164 | 165 | # --- Common Artifacts / Descriptions (Use cautiously) --- 166 | MOTION 167 | ARTIFACT 168 | METAL 169 | STREAKING 170 | BEAM HARDENING 171 | 172 | # --- Add institution-specific, non-PHI identifiers if necessary --- 173 | # E.g., SCANNER01, RAD_ROOM_A, PROTOCOL_ABC 174 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/whitelists/dx.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on DX/CR (X-Ray) images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | BILAT 14 | 15 | # --- Orientation / Position --- 16 | AP # Anteroposterior 17 | PA # Posteroanterior 18 | LAT # Lateral 19 | OBL # Oblique 20 | DECUB # Decubitus 21 | SUPINE 22 | PRONE 23 | ERECT 24 | SEMI-ERECT 25 | STANDING 26 | SITTING 27 | RECUMBENT 28 | WEIGHT BEARING 29 | WB 30 | NON WEIGHT BEARING 31 | NWB 32 | PORTABLE 33 | PORT 34 | BEDSIDE 35 | MOBILE 36 | INSPIRATION 37 | EXPIRATION 38 | FLEXION 39 | EXTENSION 40 | INTERNAL ROTATION 41 | EXTERNAL ROTATION 42 | AXIAL 43 | CEPHALAD 44 | CAUDAD 45 | 46 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 47 | HEAD 48 | SKULL 49 | NECK 50 | CHEST 51 | CXR # Chest X-Ray 52 | KUB # Kidneys, Ureters, Bladder 53 | ABDOMEN 54 | PELVIS 55 | SPINE 56 | CERVICAL 57 | THORACIC 58 | LUMBAR 59 | SACRUM 60 | COCCYX 61 | SHOULDER 62 | ELBOW 63 | WRIST 64 | HAND 65 | HIP 66 | KNEE 67 | ANKLE 68 | FOOT 69 | EXTREMITY 70 | UPPER 71 | LOWER 72 | RIBS 73 | 74 | # --- Technical Parameters --- 75 | DX 76 | CR 77 | DR 78 | XRAY 79 | X-RAY 80 | PORTABLE 81 | GRID 82 | NO GRID 83 | AEC # Automatic Exposure Control 84 | MANUAL 85 | KV 86 | KVP 87 | MA 88 | MAS 89 | EXPOSURE 90 | TIME 91 | SID # Source-to-Image Distance 92 | FFD # Film-Focus Distance 93 | MAG # Magnification 94 | TECH 95 | TECHNOLOGIST 96 | SCOUT 97 | 98 | # --- Miscellaneous --- 99 | SERIES 100 | IMAGE 101 | IMG 102 | VIEW 103 | PROJECTION 104 | NO 105 | NUM 106 | NUMBER 107 | EXAM DATE 108 | EXAM TIME 109 | TABLE 110 | HEIGHT 111 | POS 112 | POSITION 113 | REF 114 | REFERENCE 115 | NONE 116 | N/A 117 | VARIOUS 118 | SEE REPORT 119 | CLINICAL HISTORY 120 | COMPARISON 121 | PREVIOUS 122 | PRIOR 123 | 124 | # --- Common Artifacts / Descriptions (Use cautiously) --- 125 | MOTION 126 | ARTIFACT 127 | BLUR 128 | CLOTHING 129 | JEWELRY 130 | FOREIGN BODY 131 | IMPLANT 132 | PACEMAKER 133 | LINE 134 | TUBE 135 | 136 | # --- Add institution-specific, non-PHI identifiers if necessary --- 137 | # E.g., XRAY_ROOM_1, PORTABLE_UNIT_3 138 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/en_US/whitelists/us.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on Ultrasound (US) images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | BILAT 14 | 15 | # --- Orientation / Position --- 16 | SAG # Sagittal 17 | TRV # Transverse 18 | TRA # Transverse 19 | COR # Coronal 20 | LONG # Longitudinal 21 | AXIAL 22 | AX 23 | OBLIQUE 24 | SUPINE 25 | PRONE 26 | DECUBITUS 27 | LLD # Left Lateral Decubitus 28 | RLD # Right Lateral Decubitus 29 | ERECT 30 | SEMI-ERECT 31 | SITTING 32 | 33 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 34 | HEAD 35 | NECK 36 | THYROID 37 | CAROTID 38 | CHEST 39 | BREAST 40 | ABDOMEN 41 | LIVER 42 | GALLBLADDER 43 | GB 44 | CBD # Common Bile Duct 45 | PANCREAS 46 | SPLEEN 47 | KIDNEY 48 | RENAL 49 | AORTA 50 | IVC 51 | PELVIS 52 | UTERUS 53 | OVARY 54 | PROSTATE 55 | TESTIS 56 | SCROTUM 57 | BLADDER 58 | EXTREMITY 59 | ARM 60 | LEG 61 | VENOUS 62 | ARTERIAL 63 | DVT # Deep Vein Thrombosis 64 | APPENDIX 65 | HEART # Echocardiography terms often differ significantly 66 | FETAL 67 | OB # Obstetrics 68 | GYN # Gynecology 69 | 70 | # --- Scanner / Technical Parameters --- 71 | US 72 | ULTRA SOUND 73 | SONO 74 | SONOGRAM 75 | PROBE 76 | TRANSDUCER 77 | LINEAR 78 | CURVED 79 | SECTOR 80 | ENDO # Endocavitary (e.g., Endovaginal, Endorectal) 81 | EV # Endovaginal 82 | ER # Endorectal 83 | FREQ # Frequency 84 | MHZ # Megahertz 85 | GAIN 86 | TGC # Time Gain Compensation 87 | DEPTH 88 | FOCUS 89 | FOV 90 | ZOOM 91 | HARMONIC 92 | THI # Tissue Harmonic Imaging 93 | COMPOUND 94 | POWER 95 | DOPPLER 96 | COLOR 97 | CD # Color Doppler 98 | PWR # Power Doppler 99 | PW # Pulsed Wave Doppler 100 | CW # Continuous Wave Doppler 101 | SPECTRAL 102 | VEL # Velocity 103 | PRF # Pulse Repetition Frequency 104 | FILTER 105 | SCALE 106 | ANGLE 107 | CURSOR 108 | CALIPER 109 | MEASURE 110 | DIST # Distance 111 | AREA 112 | VOLUME 113 | VOL 114 | MI # Mechanical Index 115 | TIS # Thermal Index Soft Tissue 116 | TIB # Thermal Index Bone 117 | TIC # Thermal Index Cranial Bone 118 | FR # Frame Rate 119 | FPS # Frames Per Second 120 | GRAYSCALE 121 | B MODE 122 | M MODE 123 | 124 | # --- Measurements / Units --- 125 | MM 126 | CM 127 | M/S # Meters per second 128 | CM/S # Centimeters per second 129 | KHZ # Kilohertz 130 | HZ # Hertz 131 | DEG # Degrees 132 | 133 | # --- Miscellaneous --- 134 | SERIES 135 | IMAGE 136 | IMG 137 | CINE 138 | LOOP 139 | CLIP 140 | VIEW 141 | PLANE 142 | SCAN 143 | NO 144 | NUM 145 | NUMBER 146 | EXAM DATE 147 | EXAM TIME 148 | ACQ DATE 149 | ACQ TIME 150 | POS 151 | POSITION 152 | REF 153 | REFERENCE 154 | NONE 155 | N/A 156 | VARIOUS 157 | SEE REPORT 158 | CLINICAL HISTORY 159 | PROTOCOL 160 | AUTO 161 | FREEZE 162 | PRINT 163 | STORE 164 | 165 | # --- Common Artifacts / Descriptions (Use cautiously) --- 166 | SHADOWING 167 | ENHANCEMENT 168 | REVERBERATION 169 | MIRROR 170 | ARTIFACT 171 | 172 | # --- Add institution-specific, non-PHI identifiers if necessary --- 173 | # E.g., US_ROOM_3, SONOGRAPHER_ID_XYZ 174 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/3_gestión de proyectos.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 |
7 | 8 |

Gestión de Proyectos de Anonimización

9 | 10 |

Creación de un Nuevo Proyecto

11 | 12 | 16 | 17 |

Cierre de un Proyecto

18 | 19 | 22 | 23 |

Reapertura de un Proyecto

24 | 25 | 30 | 31 |

Clonación de un Proyecto

32 | 33 | 41 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/6_licencia.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Licencia

5 | El software RSNA DICOM Anonymizer se publica bajo la 6 | Licencia Pública de RSNA. 7 |

8 |

Dependencias

9 |
    10 |
  1. CustomTkinter
  2. 11 |
  3. Python Imaging Library (PIL): Pillow
  4. 12 |
  5. tkhtmlview
  6. 13 |
  7. pydicom
  8. 14 |
  9. pynetdicom
  10. 15 |
  11. ifaddr
  12. 16 |
  13. boto3
  14. 17 |
  15. pywin32-ctypes
  16. 18 |
  17. pefile
  18. 19 |
  19. openpyxl
  20. 20 |
21 |

Licencias de código abierto de las dependencias

22 | El software mencionado anteriormente, incluido en el RSNA DICOM Anonymizer, se publica bajo las siguientes licencias: 23 | 29 | 30 |
31 | 32 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/AWSCognitoCredenciales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/AWSCognitoCredenciales.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/AbrirReciente.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/AbrirReciente.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/CerrarProyecto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/CerrarProyecto.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ClasesDeAlmacenamiento.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ClasesDeAlmacenamiento.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ClonaciónDeProyecto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ClonaciónDeProyecto.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ConfiguraciónDelProyecto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ConfiguraciónDelProyecto.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ConsultarRecuperarImportar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ConsultarRecuperarImportar.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/DatosDeBúsquedaDePaciente.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/DatosDeBúsquedaDePaciente.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/DirectorioDeAlmacenamiento.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/DirectorioDeAlmacenamiento.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ExportarEstudiosAWS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ExportarEstudiosAWS.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ExportarEstudiosEstado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ExportarEstudiosEstado.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/GuardarBúsquedaDePaciente.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/GuardarBúsquedaDePaciente.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ImportarArchivos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ImportarArchivos.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ImportarArchivosDiálogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ImportarArchivosDiálogo.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ImportarEstudiosDiálogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ImportarEstudiosDiálogo.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ImportarEstudiosResultado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ImportarEstudiosResultado.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/Modalidades.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/Modalidades.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/NivelesDeRegistro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/NivelesDeRegistro.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/NuevoProyecto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/NuevoProyecto.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/PanelDeControl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/PanelDeControl.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/SeleccionarArchivos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/SeleccionarArchivos.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/SeleccionarDirectorio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/SeleccionarDirectorio.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/SeleccionarImportar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/SeleccionarImportar.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ServidorDeConsulta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ServidorDeConsulta.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ServidorDeExportación.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ServidorDeExportación.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/ServidorLocal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/ServidorLocal.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/SintaxisDeTransferencia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/SintaxisDeTransferencia.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/TimeoutsDeRed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/TimeoutsDeRed.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/Welcome_es_osx_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/Welcome_es_osx_light.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html/images/Welcome_es_win_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/es/html/images/Welcome_es_win_light.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/html_bak/6_licencia.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Licencia

5 | El software RSNA DICOM Anonymizer se publica bajo la 6 | Licencia Pública de RSNA. 7 |

8 |

Dependencias

9 |
    10 |
  1. CustomTkinter
  2. 11 |
  3. Python Imaging Library (PIL): Pillow
  4. 12 |
  5. tkhtmlview
  6. 13 |
  7. pydicom
  8. 14 |
  9. pynetdicom
  10. 15 |
  11. ifaddr
  12. 16 |
  13. boto3
  14. 17 |
  15. pywin32-ctypes
  16. 18 |
  17. pefile
  18. 19 |
  19. openpyxl
  20. 20 |
21 |

Licencias de código abierto de las dependencias

22 | El software mencionado anteriormente, incluido en el RSNA DICOM Anonymizer, se publica bajo las siguientes licencias: 23 | 29 | 30 |
31 | 32 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/whitelists/ct.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on CT images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | 14 | # --- Orientation / Position --- 15 | AX 16 | AXIAL 17 | COR 18 | CORONAL 19 | SAG 20 | SAGITTAL 21 | OBLIQUE 22 | SUPINE 23 | PRONE 24 | DECUBITUS 25 | ERECT 26 | SEMI-ERECT 27 | HEAD FIRST 28 | FEET FIRST 29 | HF 30 | FF 31 | HFS 32 | FFS 33 | HFP 34 | FFP 35 | 36 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 37 | HEAD 38 | NECK 39 | CHEST 40 | ABDOMEN 41 | PELVIS 42 | SPINE 43 | EXTREMITY 44 | BRAIN 45 | LUNG 46 | LIVER 47 | KIDNEY 48 | HEART 49 | AORTA 50 | VESSEL 51 | 52 | # --- Scanner / Technical Parameters --- 53 | CT 54 | SCAN 55 | SCOUT 56 | TOPOGRAM 57 | SURVIEW 58 | SCANOGRAM 59 | HELICAL 60 | SEQUENTIAL 61 | VOLUME 62 | ACQ 63 | RECON 64 | SLICE 65 | THICKNESS 66 | SL 67 | THK 68 | FOV 69 | DFOV 70 | ZOOM 71 | WW 72 | WL 73 | WINDOW 74 | LEVEL 75 | WIDTH 76 | KV 77 | KVP 78 | MA 79 | MAS 80 | TIME 81 | ROT TIME 82 | ROTATION 83 | PITCH 84 | NOISE 85 | INDEX 86 | CTDI 87 | CTDI VOL 88 | DLP 89 | KERNEL 90 | FILTER 91 | STANDARD 92 | SOFT 93 | BONE 94 | LUNG 95 | EDGE 96 | SHARP 97 | SMOOTH 98 | ITERATIVE 99 | IR 100 | ASIR 101 | MBIR 102 | IMR 103 | EXPOSURE 104 | 105 | # --- Contrast / Timing --- 106 | CONTRAST 107 | CONT 108 | WITH CONTRAST 109 | W CONTRAST 110 | W/C 111 | WITHOUT CONTRAST 112 | WO CONTRAST 113 | NON CON 114 | NON-CON 115 | PRE 116 | POST 117 | PRE CONTRAST 118 | POST CONTRAST 119 | ARTERIAL 120 | ART 121 | VENOUS 122 | VEN 123 | DELAY 124 | DELAYED 125 | NEPHROGRAPHIC 126 | EXCRETORY 127 | PORTAL 128 | PV 129 | ORAL 130 | IV 131 | BOLUS 132 | INJECTION 133 | 134 | # --- Measurements / Units --- 135 | MM 136 | CM 137 | HU # Hounsfield Unit 138 | SUV # Standardized Uptake Value (PET/CT) 139 | 140 | # --- Miscellaneous --- 141 | SERIES 142 | IMAGE 143 | IMG 144 | NO 145 | NUM 146 | NUMBER 147 | SCAN DATE 148 | SCAN TIME 149 | ACQ DATE 150 | ACQ TIME 151 | TABLE 152 | HEIGHT 153 | POS 154 | POSITION 155 | REF 156 | REFERENCE 157 | NONE 158 | N/A 159 | VARIOUS 160 | SEE REPORT 161 | CLINICAL HISTORY 162 | PROTOCOL 163 | AUTO 164 | 165 | # --- Common Artifacts / Descriptions (Use cautiously) --- 166 | MOTION 167 | ARTIFACT 168 | METAL 169 | STREAKING 170 | BEAM HARDENING 171 | 172 | # --- Add institution-specific, non-PHI identifiers if necessary --- 173 | # E.g., SCANNER01, RAD_ROOM_A, PROTOCOL_ABC 174 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/whitelists/dx.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on DX/CR (X-Ray) images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | BILAT 14 | 15 | # --- Orientation / Position --- 16 | AP # Anteroposterior 17 | PA # Posteroanterior 18 | LAT # Lateral 19 | OBL # Oblique 20 | DECUB # Decubitus 21 | SUPINE 22 | PRONE 23 | ERECT 24 | SEMI-ERECT 25 | STANDING 26 | SITTING 27 | RECUMBENT 28 | WEIGHT BEARING 29 | WB 30 | NON WEIGHT BEARING 31 | NWB 32 | PORTABLE 33 | PORT 34 | BEDSIDE 35 | MOBILE 36 | INSPIRATION 37 | EXPIRATION 38 | FLEXION 39 | EXTENSION 40 | INTERNAL ROTATION 41 | EXTERNAL ROTATION 42 | AXIAL 43 | CEPHALAD 44 | CAUDAD 45 | 46 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 47 | HEAD 48 | SKULL 49 | NECK 50 | CHEST 51 | CXR # Chest X-Ray 52 | KUB # Kidneys, Ureters, Bladder 53 | ABDOMEN 54 | PELVIS 55 | SPINE 56 | CERVICAL 57 | THORACIC 58 | LUMBAR 59 | SACRUM 60 | COCCYX 61 | SHOULDER 62 | ELBOW 63 | WRIST 64 | HAND 65 | HIP 66 | KNEE 67 | ANKLE 68 | FOOT 69 | EXTREMITY 70 | UPPER 71 | LOWER 72 | RIBS 73 | 74 | # --- Technical Parameters --- 75 | DX 76 | CR 77 | DR 78 | XRAY 79 | X-RAY 80 | PORTABLE 81 | GRID 82 | NO GRID 83 | AEC # Automatic Exposure Control 84 | MANUAL 85 | KV 86 | KVP 87 | MA 88 | MAS 89 | EXPOSURE 90 | TIME 91 | SID # Source-to-Image Distance 92 | FFD # Film-Focus Distance 93 | MAG # Magnification 94 | TECH 95 | TECHNOLOGIST 96 | SCOUT 97 | 98 | # --- Miscellaneous --- 99 | SERIES 100 | IMAGE 101 | IMG 102 | VIEW 103 | PROJECTION 104 | NO 105 | NUM 106 | NUMBER 107 | EXAM DATE 108 | EXAM TIME 109 | TABLE 110 | HEIGHT 111 | POS 112 | POSITION 113 | REF 114 | REFERENCE 115 | NONE 116 | N/A 117 | VARIOUS 118 | SEE REPORT 119 | CLINICAL HISTORY 120 | COMPARISON 121 | PREVIOUS 122 | PRIOR 123 | 124 | # --- Common Artifacts / Descriptions (Use cautiously) --- 125 | MOTION 126 | ARTIFACT 127 | BLUR 128 | CLOTHING 129 | JEWELRY 130 | FOREIGN BODY 131 | IMPLANT 132 | PACEMAKER 133 | LINE 134 | TUBE 135 | 136 | # --- Add institution-specific, non-PHI identifiers if necessary --- 137 | # E.g., XRAY_ROOM_1, PORTABLE_UNIT_3 138 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/es/whitelists/us.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on Ultrasound (US) images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | BILAT 14 | 15 | # --- Orientation / Position --- 16 | SAG # Sagittal 17 | TRV # Transverse 18 | TRA # Transverse 19 | COR # Coronal 20 | LONG # Longitudinal 21 | AXIAL 22 | AX 23 | OBLIQUE 24 | SUPINE 25 | PRONE 26 | DECUBITUS 27 | LLD # Left Lateral Decubitus 28 | RLD # Right Lateral Decubitus 29 | ERECT 30 | SEMI-ERECT 31 | SITTING 32 | 33 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 34 | HEAD 35 | NECK 36 | THYROID 37 | CAROTID 38 | CHEST 39 | BREAST 40 | ABDOMEN 41 | LIVER 42 | GALLBLADDER 43 | GB 44 | CBD # Common Bile Duct 45 | PANCREAS 46 | SPLEEN 47 | KIDNEY 48 | RENAL 49 | AORTA 50 | IVC 51 | PELVIS 52 | UTERUS 53 | OVARY 54 | PROSTATE 55 | TESTIS 56 | SCROTUM 57 | BLADDER 58 | EXTREMITY 59 | ARM 60 | LEG 61 | VENOUS 62 | ARTERIAL 63 | DVT # Deep Vein Thrombosis 64 | APPENDIX 65 | HEART # Echocardiography terms often differ significantly 66 | FETAL 67 | OB # Obstetrics 68 | GYN # Gynecology 69 | 70 | # --- Scanner / Technical Parameters --- 71 | US 72 | ULTRA SOUND 73 | SONO 74 | SONOGRAM 75 | PROBE 76 | TRANSDUCER 77 | LINEAR 78 | CURVED 79 | SECTOR 80 | ENDO # Endocavitary (e.g., Endovaginal, Endorectal) 81 | EV # Endovaginal 82 | ER # Endorectal 83 | FREQ # Frequency 84 | MHZ # Megahertz 85 | GAIN 86 | TGC # Time Gain Compensation 87 | DEPTH 88 | FOCUS 89 | FOV 90 | ZOOM 91 | HARMONIC 92 | THI # Tissue Harmonic Imaging 93 | COMPOUND 94 | POWER 95 | DOPPLER 96 | COLOR 97 | CD # Color Doppler 98 | PWR # Power Doppler 99 | PW # Pulsed Wave Doppler 100 | CW # Continuous Wave Doppler 101 | SPECTRAL 102 | VEL # Velocity 103 | PRF # Pulse Repetition Frequency 104 | FILTER 105 | SCALE 106 | ANGLE 107 | CURSOR 108 | CALIPER 109 | MEASURE 110 | DIST # Distance 111 | AREA 112 | VOLUME 113 | VOL 114 | MI # Mechanical Index 115 | TIS # Thermal Index Soft Tissue 116 | TIB # Thermal Index Bone 117 | TIC # Thermal Index Cranial Bone 118 | FR # Frame Rate 119 | FPS # Frames Per Second 120 | GRAYSCALE 121 | B MODE 122 | M MODE 123 | 124 | # --- Measurements / Units --- 125 | MM 126 | CM 127 | M/S # Meters per second 128 | CM/S # Centimeters per second 129 | KHZ # Kilohertz 130 | HZ # Hertz 131 | DEG # Degrees 132 | 133 | # --- Miscellaneous --- 134 | SERIES 135 | IMAGE 136 | IMG 137 | CINE 138 | LOOP 139 | CLIP 140 | VIEW 141 | PLANE 142 | SCAN 143 | NO 144 | NUM 145 | NUMBER 146 | EXAM DATE 147 | EXAM TIME 148 | ACQ DATE 149 | ACQ TIME 150 | POS 151 | POSITION 152 | REF 153 | REFERENCE 154 | NONE 155 | N/A 156 | VARIOUS 157 | SEE REPORT 158 | CLINICAL HISTORY 159 | PROTOCOL 160 | AUTO 161 | FREEZE 162 | PRINT 163 | STORE 164 | 165 | # --- Common Artifacts / Descriptions (Use cautiously) --- 166 | SHADOWING 167 | ENHANCEMENT 168 | REVERBERATION 169 | MIRROR 170 | ARTIFACT 171 | 172 | # --- Add institution-specific, non-PHI identifiers if necessary --- 173 | # E.g., US_ROOM_3, SONOGRAPHER_ID_XYZ 174 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/extract_translations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Ensure gettext is installed: 4 | # Linux: sudo apt-get install gettext 5 | # Mac: brew install gettext 6 | # Windows: https://mlocati.github.io/articles/gettext-iconv-windows.html or choco install gettext 7 | 8 | # Define the source directory and the output .pot file 9 | SRC_DIR="../.." 10 | POT_FILE="messages.pot" 11 | 12 | # Find all .py files in the source directory and extract translatable strings 13 | find $SRC_DIR -name '*.py' | xargs xgettext -v -d messages -o $POT_FILE --from-code UTF-8 -L Python --omit-header --no-wrap --no-location 14 | 15 | # To initialise a new language translation file, run the following command: 16 | msginit -l en_US -o en_US/LC_MESSAGES/messages.po -i messages.pot --no-translator --no-wrap 17 | #msginit -l es -o es/LC_MESSAGES/messages.po -i messages.pot --no-translator --no-wrap 18 | #msginit -l de -o de/LC_MESSAGES/messages.po -i messages.pot --no-translator --no-wrap 19 | #msginit -l fr -o de/LC_MESSAGES/messages.po -i messages.pot --no-translator --no-wrap -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/fr/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/fr/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/fr/html/3_gestion de projet.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 |
7 | 8 |

Gestion des Projets d'Anonymisation

9 | 10 |

Création d'un Nouveau Projet

11 | 12 | 16 | 17 |

Fermeture d'un Projet

18 | 19 | 22 | 23 |

Réouverture d'un Projet

24 | 25 | 30 | 31 |

Clonage d'un Projet

32 | 33 | 41 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/fr/html/6_licence.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Licence

5 | Le logiciel RSNA DICOM Anonymizer est publié sous la 6 | Licence Publique de la RSNA. 7 |

8 |

Dépendances

9 |
    10 |
  1. CustomTkinter
  2. 11 |
  3. Python Imaging Library (PIL): Pillow
  4. 12 |
  5. tkhtmlview
  6. 13 |
  7. pydicom
  8. 14 |
  9. pynetdicom
  10. 15 |
  11. ifaddr
  12. 16 |
  13. boto3
  14. 17 |
  15. pywin32-ctypes
  16. 18 |
  17. pefile
  18. 19 |
  19. openpyxl
  20. 20 |
21 |

Licences open source des dépendances

22 | Les logiciels ci-dessus, inclus dans le RSNA DICOM Anonymizer, sont publiés sous les licences suivantes : 23 | 29 | 30 |
31 | 32 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/fr/html/images/Welcome_fr_osx_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/fr/html/images/Welcome_fr_osx_light.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/fr/html/images/Welcome_fr_win_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/assets/locales/fr/html/images/Welcome_fr_win_light.png -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/fr/whitelists/ct.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on CT images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | 14 | # --- Orientation / Position --- 15 | AX 16 | AXIAL 17 | COR 18 | CORONAL 19 | SAG 20 | SAGITTAL 21 | OBLIQUE 22 | SUPINE 23 | PRONE 24 | DECUBITUS 25 | ERECT 26 | SEMI-ERECT 27 | HEAD FIRST 28 | FEET FIRST 29 | HF 30 | FF 31 | HFS 32 | FFS 33 | HFP 34 | FFP 35 | 36 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 37 | HEAD 38 | NECK 39 | CHEST 40 | ABDOMEN 41 | PELVIS 42 | SPINE 43 | EXTREMITY 44 | BRAIN 45 | LUNG 46 | LIVER 47 | KIDNEY 48 | HEART 49 | AORTA 50 | VESSEL 51 | 52 | # --- Scanner / Technical Parameters --- 53 | CT 54 | SCAN 55 | SCOUT 56 | TOPOGRAM 57 | SURVIEW 58 | SCANOGRAM 59 | HELICAL 60 | SEQUENTIAL 61 | VOLUME 62 | ACQ 63 | RECON 64 | SLICE 65 | THICKNESS 66 | SL 67 | THK 68 | FOV 69 | DFOV 70 | ZOOM 71 | WW 72 | WL 73 | WINDOW 74 | LEVEL 75 | WIDTH 76 | KV 77 | KVP 78 | MA 79 | MAS 80 | TIME 81 | ROT TIME 82 | ROTATION 83 | PITCH 84 | NOISE 85 | INDEX 86 | CTDI 87 | CTDI VOL 88 | DLP 89 | KERNEL 90 | FILTER 91 | STANDARD 92 | SOFT 93 | BONE 94 | LUNG 95 | EDGE 96 | SHARP 97 | SMOOTH 98 | ITERATIVE 99 | IR 100 | ASIR 101 | MBIR 102 | IMR 103 | EXPOSURE 104 | 105 | # --- Contrast / Timing --- 106 | CONTRAST 107 | CONT 108 | WITH CONTRAST 109 | W CONTRAST 110 | W/C 111 | WITHOUT CONTRAST 112 | WO CONTRAST 113 | NON CON 114 | NON-CON 115 | PRE 116 | POST 117 | PRE CONTRAST 118 | POST CONTRAST 119 | ARTERIAL 120 | ART 121 | VENOUS 122 | VEN 123 | DELAY 124 | DELAYED 125 | NEPHROGRAPHIC 126 | EXCRETORY 127 | PORTAL 128 | PV 129 | ORAL 130 | IV 131 | BOLUS 132 | INJECTION 133 | 134 | # --- Measurements / Units --- 135 | MM 136 | CM 137 | HU # Hounsfield Unit 138 | SUV # Standardized Uptake Value (PET/CT) 139 | 140 | # --- Miscellaneous --- 141 | SERIES 142 | IMAGE 143 | IMG 144 | NO 145 | NUM 146 | NUMBER 147 | SCAN DATE 148 | SCAN TIME 149 | ACQ DATE 150 | ACQ TIME 151 | TABLE 152 | HEIGHT 153 | POS 154 | POSITION 155 | REF 156 | REFERENCE 157 | NONE 158 | N/A 159 | VARIOUS 160 | SEE REPORT 161 | CLINICAL HISTORY 162 | PROTOCOL 163 | AUTO 164 | 165 | # --- Common Artifacts / Descriptions (Use cautiously) --- 166 | MOTION 167 | ARTIFACT 168 | METAL 169 | STREAKING 170 | BEAM HARDENING 171 | 172 | # --- Add institution-specific, non-PHI identifiers if necessary --- 173 | # E.g., SCANNER01, RAD_ROOM_A, PROTOCOL_ABC 174 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/fr/whitelists/dx.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on DX/CR (X-Ray) images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | BILAT 14 | 15 | # --- Orientation / Position --- 16 | AP # Anteroposterior 17 | PA # Posteroanterior 18 | LAT # Lateral 19 | OBL # Oblique 20 | DECUB # Decubitus 21 | SUPINE 22 | PRONE 23 | ERECT 24 | SEMI-ERECT 25 | STANDING 26 | SITTING 27 | RECUMBENT 28 | WEIGHT BEARING 29 | WB 30 | NON WEIGHT BEARING 31 | NWB 32 | PORTABLE 33 | PORT 34 | BEDSIDE 35 | MOBILE 36 | INSPIRATION 37 | EXPIRATION 38 | FLEXION 39 | EXTENSION 40 | INTERNAL ROTATION 41 | EXTERNAL ROTATION 42 | AXIAL 43 | CEPHALAD 44 | CAUDAD 45 | 46 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 47 | HEAD 48 | SKULL 49 | NECK 50 | CHEST 51 | CXR # Chest X-Ray 52 | KUB # Kidneys, Ureters, Bladder 53 | ABDOMEN 54 | PELVIS 55 | SPINE 56 | CERVICAL 57 | THORACIC 58 | LUMBAR 59 | SACRUM 60 | COCCYX 61 | SHOULDER 62 | ELBOW 63 | WRIST 64 | HAND 65 | HIP 66 | KNEE 67 | ANKLE 68 | FOOT 69 | EXTREMITY 70 | UPPER 71 | LOWER 72 | RIBS 73 | 74 | # --- Technical Parameters --- 75 | DX 76 | CR 77 | DR 78 | XRAY 79 | X-RAY 80 | PORTABLE 81 | GRID 82 | NO GRID 83 | AEC # Automatic Exposure Control 84 | MANUAL 85 | KV 86 | KVP 87 | MA 88 | MAS 89 | EXPOSURE 90 | TIME 91 | SID # Source-to-Image Distance 92 | FFD # Film-Focus Distance 93 | MAG # Magnification 94 | TECH 95 | TECHNOLOGIST 96 | SCOUT 97 | 98 | # --- Miscellaneous --- 99 | SERIES 100 | IMAGE 101 | IMG 102 | VIEW 103 | PROJECTION 104 | NO 105 | NUM 106 | NUMBER 107 | EXAM DATE 108 | EXAM TIME 109 | TABLE 110 | HEIGHT 111 | POS 112 | POSITION 113 | REF 114 | REFERENCE 115 | NONE 116 | N/A 117 | VARIOUS 118 | SEE REPORT 119 | CLINICAL HISTORY 120 | COMPARISON 121 | PREVIOUS 122 | PRIOR 123 | 124 | # --- Common Artifacts / Descriptions (Use cautiously) --- 125 | MOTION 126 | ARTIFACT 127 | BLUR 128 | CLOTHING 129 | JEWELRY 130 | FOREIGN BODY 131 | IMPLANT 132 | PACEMAKER 133 | LINE 134 | TUBE 135 | 136 | # --- Add institution-specific, non-PHI identifiers if necessary --- 137 | # E.g., XRAY_ROOM_1, PORTABLE_UNIT_3 138 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/fr/whitelists/us.txt: -------------------------------------------------------------------------------- 1 | # Whitelist for common terms found on Ultrasound (US) images 2 | # One term/phrase per line. Case-insensitive matching is assumed after loading. 3 | # Lines starting with # are comments and will be ignored. 4 | 5 | # --- Laterality --- 6 | L 7 | R 8 | LEFT 9 | RIGHT 10 | LT 11 | RT 12 | BILATERAL 13 | BILAT 14 | 15 | # --- Orientation / Position --- 16 | SAG # Sagittal 17 | TRV # Transverse 18 | TRA # Transverse 19 | COR # Coronal 20 | LONG # Longitudinal 21 | AXIAL 22 | AX 23 | OBLIQUE 24 | SUPINE 25 | PRONE 26 | DECUBITUS 27 | LLD # Left Lateral Decubitus 28 | RLD # Right Lateral Decubitus 29 | ERECT 30 | SEMI-ERECT 31 | SITTING 32 | 33 | # --- Anatomy (Very General - Add specific non-PHI terms carefully) --- 34 | HEAD 35 | NECK 36 | THYROID 37 | CAROTID 38 | CHEST 39 | BREAST 40 | ABDOMEN 41 | LIVER 42 | GALLBLADDER 43 | GB 44 | CBD # Common Bile Duct 45 | PANCREAS 46 | SPLEEN 47 | KIDNEY 48 | RENAL 49 | AORTA 50 | IVC 51 | PELVIS 52 | UTERUS 53 | OVARY 54 | PROSTATE 55 | TESTIS 56 | SCROTUM 57 | BLADDER 58 | EXTREMITY 59 | ARM 60 | LEG 61 | VENOUS 62 | ARTERIAL 63 | DVT # Deep Vein Thrombosis 64 | APPENDIX 65 | HEART # Echocardiography terms often differ significantly 66 | FETAL 67 | OB # Obstetrics 68 | GYN # Gynecology 69 | 70 | # --- Scanner / Technical Parameters --- 71 | US 72 | ULTRA SOUND 73 | SONO 74 | SONOGRAM 75 | PROBE 76 | TRANSDUCER 77 | LINEAR 78 | CURVED 79 | SECTOR 80 | ENDO # Endocavitary (e.g., Endovaginal, Endorectal) 81 | EV # Endovaginal 82 | ER # Endorectal 83 | FREQ # Frequency 84 | MHZ # Megahertz 85 | GAIN 86 | TGC # Time Gain Compensation 87 | DEPTH 88 | FOCUS 89 | FOV 90 | ZOOM 91 | HARMONIC 92 | THI # Tissue Harmonic Imaging 93 | COMPOUND 94 | POWER 95 | DOPPLER 96 | COLOR 97 | CD # Color Doppler 98 | PWR # Power Doppler 99 | PW # Pulsed Wave Doppler 100 | CW # Continuous Wave Doppler 101 | SPECTRAL 102 | VEL # Velocity 103 | PRF # Pulse Repetition Frequency 104 | FILTER 105 | SCALE 106 | ANGLE 107 | CURSOR 108 | CALIPER 109 | MEASURE 110 | DIST # Distance 111 | AREA 112 | VOLUME 113 | VOL 114 | MI # Mechanical Index 115 | TIS # Thermal Index Soft Tissue 116 | TIB # Thermal Index Bone 117 | TIC # Thermal Index Cranial Bone 118 | FR # Frame Rate 119 | FPS # Frames Per Second 120 | GRAYSCALE 121 | B MODE 122 | M MODE 123 | 124 | # --- Measurements / Units --- 125 | MM 126 | CM 127 | M/S # Meters per second 128 | CM/S # Centimeters per second 129 | KHZ # Kilohertz 130 | HZ # Hertz 131 | DEG # Degrees 132 | 133 | # --- Miscellaneous --- 134 | SERIES 135 | IMAGE 136 | IMG 137 | CINE 138 | LOOP 139 | CLIP 140 | VIEW 141 | PLANE 142 | SCAN 143 | NO 144 | NUM 145 | NUMBER 146 | EXAM DATE 147 | EXAM TIME 148 | ACQ DATE 149 | ACQ TIME 150 | POS 151 | POSITION 152 | REF 153 | REFERENCE 154 | NONE 155 | N/A 156 | VARIOUS 157 | SEE REPORT 158 | CLINICAL HISTORY 159 | PROTOCOL 160 | AUTO 161 | FREEZE 162 | PRINT 163 | STORE 164 | 165 | # --- Common Artifacts / Descriptions (Use cautiously) --- 166 | SHADOWING 167 | ENHANCEMENT 168 | REVERBERATION 169 | MIRROR 170 | ARTIFACT 171 | 172 | # --- Add institution-specific, non-PHI identifiers if necessary --- 173 | # E.g., US_ROOM_3, SONOGRAPHER_ID_XYZ 174 | -------------------------------------------------------------------------------- /src/anonymizer/assets/locales/update_translations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Step 1: Update the .pot file, as per extract_translations.sh but with --join-existing 4 | # Define the source directory and the output .pot file 5 | SRC_DIR="../.." 6 | POT_FILE="messages.pot" 7 | # Find all .py files in the source directory and extract translatable strings 8 | find $SRC_DIR -name '*.py' | xargs xgettext -v -d messages -o $POT_FILE --from-code UTF-8 -L Python --omit-header --no-wrap --no-location 9 | 10 | # Step 2: Loop through each language directory in the locale directory 11 | for lang_dir in */LC_MESSAGES; do 12 | # Define the .po and .mo files for the current language 13 | PO_FILE="$lang_dir/messages.po" 14 | MO_FILE="$lang_dir/messages.mo" 15 | 16 | # Step 3: Merge the updated .pot file with the existing .po file 17 | if [ -f "$PO_FILE" ]; then 18 | msgmerge -U $PO_FILE messages.pot --no-wrap 19 | fi 20 | 21 | # Step 4: Compile the .po file to a .mo file 22 | if [ -f "$PO_FILE" ]; then 23 | msgfmt -cv $PO_FILE -o $MO_FILE 24 | fi 25 | done 26 | -------------------------------------------------------------------------------- /src/anonymizer/controller/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/controller/__init__.py -------------------------------------------------------------------------------- /src/anonymizer/controller/dicom_C_codes.py: -------------------------------------------------------------------------------- 1 | # C-ECHO, C-STORE, C-FIND, C-MOVE return status values used by Anonmyzier 2 | # from DICOM Standard, Part 7: 3 | # https://dicom.nema.org/medical/dicom/current/output/chtml/part07/chapter_9.html#sect_9.1.1 4 | # Non-Service Class specific statuses - PS3.7 Annex C 5 | 6 | C_SUCCESS = 0x0000 7 | C_STORE_PROCESSING_FAILURE = 0x0110 8 | C_STORE_OUT_OF_RESOURCES = 0xA700 9 | C_MOVE_UNKNOWN_AE = 0xA801 10 | C_STORE_DATASET_ERROR = 0xA900 11 | C_CANCEL = 0xFE00 12 | C_PENDING_A = 0xFF00 13 | C_PENDING_B = 0xFF01 14 | C_SOP_CLASS_INVALID = 0xC313 15 | C_WARNING = 0xB000 16 | C_FAILURE = 0xC000 17 | C_DATA_ELEMENT_DOES_NOT_EXIST = 0x0107 18 | C_STORE_UNRECOGNIZED_OPERATION = 0xC211 19 | C_STORE_DECODE_ERROR = 0xC210 20 | C_STORE_UNRECOGNIZED_OPERATION = 0xC211 21 | -------------------------------------------------------------------------------- /src/anonymizer/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/model/__init__.py -------------------------------------------------------------------------------- /src/anonymizer/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/utils/__init__.py -------------------------------------------------------------------------------- /src/anonymizer/utils/network.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides utility functions related to network operations. 3 | """ 4 | 5 | import ipaddress 6 | import socket 7 | 8 | import ifaddr 9 | 10 | 11 | def get_local_ip_addresses() -> list[str]: 12 | """ 13 | Get the list of local IP addresses. 14 | 15 | Returns: 16 | list: A list of local IP addresses as strings. 17 | """ 18 | ip_addresses = [] 19 | adapters = ifaddr.get_adapters() 20 | for adapter in adapters: 21 | for ip in adapter.ips: 22 | if isinstance(ip.ip, str) and not ip.ip.startswith("169."): 23 | ip_addresses.append(ip.ip) 24 | return ip_addresses 25 | 26 | 27 | def dns_lookup(domain_name) -> str: 28 | """ 29 | Performs a DNS lookup for the given domain name. 30 | 31 | Args: 32 | domain_name (str): The domain name to perform the DNS lookup for. 33 | 34 | Returns: 35 | str: The IP address associated with the domain name, or "_DNS Lookup Failed" if the lookup fails. 36 | """ 37 | try: 38 | return socket.gethostbyname(domain_name) 39 | except Exception: 40 | return "_DNS Lookup Failed" 41 | 42 | 43 | def is_valid_ip(ip_str) -> bool: 44 | """ 45 | Check if the given IP address is valid. 46 | 47 | Args: 48 | ip_str (str): The IP address to be checked. 49 | 50 | Returns: 51 | bool: True if the IP address is valid, False otherwise. 52 | """ 53 | try: 54 | ipaddress.ip_address(ip_str) 55 | return True 56 | except ValueError: 57 | return False 58 | -------------------------------------------------------------------------------- /src/anonymizer/utils/version.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | from pathlib import Path 3 | 4 | import toml 5 | 6 | # poetry version [command]: 7 | 8 | # patch: Increments the patch version. 9 | # Example: 1.0.0 to 1.0.1. 10 | 11 | # minor: Increments the minor version. 12 | # Example: 1.0.0 to 1.1.0. 13 | 14 | # major: Increments the major version. 15 | # Example: 1.0.0 to 2.0.0. 16 | 17 | 18 | def get_version() -> str: 19 | try: 20 | # Try to get the version from the installed package metadata 21 | return importlib.metadata.version("rsna-anonymizer") 22 | except importlib.metadata.PackageNotFoundError as import_error: 23 | # Fallback to reading the version from pyproject.toml 24 | pyproject_path = ( 25 | Path(__file__).resolve().parent.parent.parent.parent / "pyproject.toml" 26 | ) 27 | if not pyproject_path.exists(): 28 | raise FileNotFoundError("pyproject.toml not found") from import_error 29 | pyproject_data = toml.load(pyproject_path) 30 | return pyproject_data["tool"]["poetry"]["version"] 31 | -------------------------------------------------------------------------------- /src/anonymizer/view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/anonymizer/view/__init__.py -------------------------------------------------------------------------------- /src/anonymizer/view/html_view.py: -------------------------------------------------------------------------------- 1 | 2 | # List of HTML Tags supported by tkhtmlview: 3 | # see https://github.com/bauripalash/tkhtmlview?tab=readme-ov-file#html-support 4 | import re 5 | import tkinter as tk 6 | 7 | import customtkinter as ctk 8 | from tkhtmlview import HTMLScrolledText, RenderHTML 9 | 10 | 11 | class HTMLView(tk.Toplevel): 12 | """ 13 | A custom Tkinter Toplevel window for displaying HTML content. 14 | 15 | Args: 16 | parent (ctk.CTk): The parent CTk object. 17 | title (str): The title of the HTMLView window. 18 | html_file_path (str): The file path to the HTML content. 19 | 20 | Attributes: 21 | MIN_WIDTH_px (int): The minimum width of the HTMLView window in pixels. 22 | MAX_WIDTH_px (int): The maximum width of the HTMLView window in pixels. 23 | HEIGHT_LINES (int): The number of lines for the HTMLScrolledText widget. 24 | 25 | """ 26 | 27 | MIN_WIDTH_px = 100 28 | MAX_WIDTH_px = 180 29 | HEIGHT_LINES = 40 30 | 31 | def __init__(self, parent: ctk.CTk, title: str, html_file_path): 32 | super().__init__(master=parent) 33 | self._bg_color = parent._apply_appearance_mode(ctk.ThemeManager.theme["CTkFrame"]["fg_color"]) 34 | self._parent = parent 35 | self.title(title) 36 | self.html_file_path = html_file_path 37 | self._frame = ctk.CTkFrame(self) 38 | self.rowconfigure(0, weight=1) 39 | self.columnconfigure(0, weight=1) 40 | self._frame.grid(row=0, column=0, padx=10, pady=10, sticky="nswe") 41 | self._create_widgets() 42 | 43 | def _create_widgets(self): 44 | """ 45 | Create the widgets for the HTMLView window. 46 | 47 | Reads the HTML content from the file, finds all
  • elements and their content, 48 | determines the required width based on the longest
  • element, and creates 49 | an HTMLScrolledText widget to display the HTML content. 50 | 51 | """ 52 | 53 | # Read the HTML content from the file 54 | with open(self.html_file_path, "r") as file: 55 | html_content = file.read() 56 | 57 | # Find all
  • elements and their content 58 | li_elements = re.findall(r"
  • (.*?)
  • ", html_content, re.DOTALL) 59 | li_texts = [re.sub(r"<.*?>", "", li).strip() for li in li_elements] # Remove any nested HTML tags 60 | longest_li = max(li_texts, key=len, default="") 61 | required_width = len(longest_li) + 2 # Add some padding 62 | # Clip to max/min width 63 | required_width = max(self.MIN_WIDTH_px, min(required_width, self.MAX_WIDTH_px)) 64 | 65 | html_widget = HTMLScrolledText( 66 | self._frame, 67 | width=required_width, 68 | height=self.HEIGHT_LINES, 69 | wrap="word", 70 | background=self._bg_color, 71 | html=RenderHTML(self.html_file_path), 72 | ) 73 | html_widget.pack(fill="both", padx=10, pady=10, expand=True) 74 | html_widget.configure(state="disabled") 75 | -------------------------------------------------------------------------------- /src/prototyping/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/prototyping/__init__.py -------------------------------------------------------------------------------- /src/prototyping/anonymizer_stripped.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import importlib.metadata 5 | 6 | from pydicom._version import __version__ as pydicom_version 7 | from pynetdicom._version import __version__ as pynetdicom_version 8 | 9 | import tkinter as tk 10 | import customtkinter as ctk 11 | 12 | from anonymizer.utils.logging import init_logging 13 | 14 | logger = logging.getLogger() 15 | 16 | 17 | def main(): 18 | args = str(sys.argv) 19 | install_dir = os.path.dirname(os.path.realpath(__file__)) 20 | run_as_exe: bool = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") 21 | logs_dir: str = init_logging(install_dir, run_as_exe) 22 | os.chdir(install_dir) 23 | 24 | logger.info(f"cmd line args={args}") 25 | 26 | if run_as_exe: 27 | logger.info("Running as PyInstaller executable") 28 | 29 | # Retrieve the project version set by Poetry 30 | project_version: str = importlib.metadata.version("anonymizer") 31 | 32 | logger.info(f"Python Optimization Level [0,1,2]: {sys.flags.optimize}") 33 | logger.info(f"Starting ANONYMIZER Version {project_version}") 34 | logger.info(f"Running from {os.getcwd()}") 35 | logger.info(f"Logs stored in {logs_dir}") 36 | logger.info(f"Python Version: {sys.version_info.major}.{sys.version_info.minor}") 37 | logger.info(f"tkinter TkVersion: {tk.TkVersion} TclVersion: {tk.TclVersion}") 38 | logger.info(f"Customtkinter Version: {ctk.__version__}") 39 | logger.info(f"pydicom Version: {pydicom_version}, pynetdicom Version: {pynetdicom_version}") 40 | 41 | # Close Pyinstaller startup splash image on Windows 42 | if sys.platform.startswith("win"): 43 | try: 44 | import pyi_splash # type: ignore 45 | 46 | pyi_splash.close() # type: ignore 47 | except Exception: 48 | pass 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /src/prototyping/async_echo.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import tkinter as tk 3 | from tkinter import messagebox 4 | from pydicom.uid import ExplicitVRLittleEndian 5 | from pynetdicom import AE, evt, debug_logger 6 | 7 | # Enable logging 8 | debug_logger() 9 | 10 | 11 | class DICOMEchoApp: 12 | def __init__(self, root): 13 | self.root = root 14 | self.root.title("DICOM Echo") 15 | 16 | self.label = tk.Label(root, text="Enter SCP details:") 17 | self.label.pack() 18 | 19 | self.ae_title_label = tk.Label(root, text="AE Title:") 20 | self.ae_title_label.pack() 21 | self.ae_title_entry = tk.Entry(root) 22 | self.ae_title_entry.pack() 23 | 24 | self.ip_label = tk.Label(root, text="IP Address:") 25 | self.ip_label.pack() 26 | self.ip_entry = tk.Entry(root) 27 | self.ip_entry.pack() 28 | 29 | self.port_label = tk.Label(root, text="Port:") 30 | self.port_label.pack() 31 | self.port_entry = tk.Entry(root) 32 | self.port_entry.pack() 33 | 34 | self.echo_button = tk.Button(root, text="Send Echo", command=self.send_echo) 35 | self.echo_button.pack() 36 | 37 | self.loop = asyncio.get_event_loop() 38 | 39 | async def perform_echo(self, ae_title, ip, port): 40 | ae = AE() 41 | ae.add_requested_context(ExplicitVRLittleEndian) 42 | 43 | try: 44 | assoc = await ae.associate(ip, int(port), ae_title=ae_title) 45 | if assoc.is_established: 46 | status = await assoc.send_c_echo() 47 | assoc.release() 48 | 49 | if status: 50 | messagebox.showinfo("Success", f"Echo succeeded: {status.Status}") 51 | else: 52 | messagebox.showwarning("Failure", "Echo failed") 53 | else: 54 | messagebox.showwarning("Failure", "Association rejected or aborted") 55 | except Exception as e: 56 | messagebox.showerror("Error", str(e)) 57 | 58 | def send_echo(self): 59 | ae_title = self.ae_title_entry.get() 60 | ip = self.ip_entry.get() 61 | port = self.port_entry.get() 62 | 63 | # Run the perform_echo coroutine 64 | asyncio.run_coroutine_threadsafe(self.perform_echo(ae_title, ip, port), self.loop) 65 | 66 | 67 | if __name__ == "__main__": 68 | root = tk.Tk() 69 | app = DICOMEchoApp(root) 70 | 71 | # Start the Tkinter event loop in the main thread 72 | root.mainloop() 73 | -------------------------------------------------------------------------------- /src/prototyping/asyncio_ttk.py: -------------------------------------------------------------------------------- 1 | # https://www.loekvandenouweland.com/content/python-asyncio-and-tkinter.html 2 | 3 | import tkinter as tk 4 | from tkinter import ttk 5 | import asyncio 6 | 7 | 8 | class App: 9 | async def exec(self): 10 | self.window = Window(asyncio.get_event_loop()) 11 | await self.window.show() 12 | 13 | 14 | class Window(tk.Tk): 15 | def __init__(self, loop): 16 | self.loop = loop 17 | self.root = tk.Tk() 18 | self.animation = "░▒▒▒▒▒" 19 | self.label = tk.Label(text="") 20 | self.label.grid(row=0, columnspan=2, padx=(8, 8), pady=(16, 0)) 21 | self.progressbar = ttk.Progressbar(length=280) 22 | self.progressbar.grid(row=1, columnspan=2, padx=(8, 8), pady=(16, 0)) 23 | button_block = tk.Button(text="Calculate Sync", width=10, command=self.calculate_sync) 24 | button_block.grid(row=2, column=0, sticky=tk.W, padx=8, pady=8) 25 | button_non_block = tk.Button( 26 | text="Calculate Async", width=10, command=lambda: self.loop.create_task(self.calculate_async()) 27 | ) 28 | button_non_block.grid(row=2, column=1, sticky=tk.W, padx=8, pady=8) 29 | 30 | async def show(self): 31 | while True: 32 | self.label["text"] = self.animation 33 | self.animation = self.animation[1:] + self.animation[0] 34 | self.root.update() 35 | await asyncio.sleep(0.1) 36 | 37 | def calculate_sync(self): 38 | max = 3000000 39 | for i in range(1, max): 40 | self.progressbar["value"] = i / max * 100 41 | 42 | async def calculate_async(self): 43 | max = 3000000 44 | for i in range(1, max): 45 | self.progressbar["value"] = i / max * 100 46 | if i % 1000 == 0: 47 | await asyncio.sleep(0) 48 | 49 | 50 | asyncio.run(App().exec()) 51 | -------------------------------------------------------------------------------- /src/prototyping/asyncio_with_queue.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import concurrent.futures 3 | from queue import Queue 4 | from pynetdicom import AE, evt 5 | from pydicom import Dataset 6 | 7 | # Initialize a thread-safe queue for inter-task communication 8 | dicom_queue = Queue() 9 | 10 | 11 | # Define an asynchronous callback function to handle incoming DICOM files 12 | async def on_c_store(event): 13 | # Get the received DICOM dataset 14 | ds = event.dataset 15 | 16 | # Initial async processing of DICOM dataset (e.g., header extraction) 17 | await asyncio.to_thread(process_dicom, ds) 18 | 19 | # Enqueue the DICOM dataset for further processing 20 | dicom_queue.put(ds) 21 | 22 | 23 | # Perform CPU-bound processing on DICOM dataset 24 | def process_dicom(ds): 25 | # Perform CPU-bound processing here (e.g., image analysis) 26 | pass 27 | 28 | 29 | # Consume DICOM datasets from the queue and save them 30 | async def save_dicom_files(): 31 | while True: 32 | ds = dicom_queue.get() 33 | if ds: 34 | # Perform additional processing if needed 35 | # Save the DICOM dataset to a file 36 | ds.save_as("path_to_save_directory/filename.dcm") 37 | 38 | 39 | # Create an Application Entity (AE) and add the callback 40 | ae = AE() 41 | ae.add_requested_context("1.2.840.10008.5.1.4.1.1.2") # C-STORE SOP Class 42 | ae.on_c_store += on_c_store 43 | 44 | 45 | # Start the asyncio event loop 46 | async def main(): 47 | server = ae.start_server(("localhost", 11112)) 48 | await asyncio.gather( 49 | server.serve_forever(), 50 | save_dicom_files(), # Start the DICOM file saving coroutine 51 | ) 52 | 53 | 54 | if __name__ == "__main__": 55 | loop = asyncio.get_event_loop() 56 | loop.run_until_complete(main()) 57 | -------------------------------------------------------------------------------- /src/prototyping/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | import json 3 | import os 4 | 5 | _CONFIG_FILE = "model/config.json" 6 | 7 | 8 | def load(module_name) -> dict: 9 | # Default settings 10 | settings = {} 11 | 12 | # TODO: Integrity checking on configuration file, ensure valid json, etc. 13 | if os.path.isfile(_CONFIG_FILE): 14 | with open(_CONFIG_FILE, "r") as f: 15 | config = json.load(f) 16 | 17 | settings = config.get(module_name, {}) 18 | 19 | return settings 20 | 21 | 22 | def save(module_name: str, name: str, value) -> None: 23 | if os.path.isfile(_CONFIG_FILE): 24 | with open(_CONFIG_FILE, "r") as f: 25 | config_dict = json.load(f) 26 | else: 27 | config_dict = {} 28 | 29 | # Ensure module_name exists in config_dict: 30 | config_dict.setdefault(module_name, {}) 31 | 32 | config_dict[module_name][name] = value 33 | 34 | with open(_CONFIG_FILE, "w") as f: 35 | json.dump(config_dict, f, indent=4) 36 | 37 | 38 | def save_bulk(module_name: str, settings: dict) -> None: 39 | if os.path.isfile(_CONFIG_FILE): 40 | with open(_CONFIG_FILE, "r") as f: 41 | config_dict = json.load(f) 42 | else: 43 | config_dict = {} 44 | 45 | # Ensure module_name exists in config_dict: 46 | config_dict.setdefault(module_name, {}) 47 | 48 | config_dict[module_name].update(settings) 49 | 50 | with open(_CONFIG_FILE, "w") as f: 51 | json.dump(config_dict, f, indent=4) 52 | -------------------------------------------------------------------------------- /src/prototyping/contact_sheet_gif.py: -------------------------------------------------------------------------------- 1 | from tkinter import Tk, Canvas, PhotoImage 2 | from PIL import Image, ImageSequence 3 | import time 4 | 5 | 6 | def create_gif(images, duration=500, loop=0): 7 | """ 8 | Creates an animated GIF from a list of PIL Image objects. 9 | 10 | Args: 11 | images: A list of PIL Image objects. 12 | duration: The duration of each frame in milliseconds. 13 | loop: The number of times to loop the animation. 0 means infinite loop. 14 | 15 | Returns: 16 | The created GIF image. 17 | """ 18 | 19 | images[0].save("animation.gif", save_all=True, append_images=images[1:], duration=duration, loop=loop) 20 | 21 | return Image.open("animation.gif") 22 | 23 | 24 | def main(): 25 | # Create a list of PIL Image objects for the animation 26 | images = [ 27 | Image.new("RGB", (100, 100), "white"), 28 | Image.new("RGB", (100, 100), "gray"), 29 | Image.new("RGB", (100, 100), "black"), 30 | ] 31 | 32 | # Create the animated GIF 33 | gif_image = create_gif(images, duration=500) 34 | 35 | # Create the Tkinter window 36 | root = Tk() 37 | root.title("Animated GIF") 38 | 39 | # Create a canvas to display the GIF 40 | canvas = Canvas(root, width=100, height=100) 41 | canvas.pack() 42 | 43 | # Create a PhotoImage object to display the GIF 44 | photo = PhotoImage(file="animation.gif") 45 | canvas.create_image(0, 0, image=photo, anchor="nw") 46 | 47 | # Start the animation 48 | def update(): 49 | try: 50 | frame = next(ImageSequence.Iterator(gif_image)) 51 | photo.put(frame.getdata(), (0, 0)) 52 | root.after(500, update) # Schedule next update 53 | except StopIteration: 54 | # Handle reaching the end of the animation 55 | pass 56 | 57 | update() 58 | 59 | root.mainloop() 60 | 61 | 62 | if __name__ == "__main__": 63 | main() 64 | -------------------------------------------------------------------------------- /src/prototyping/contact_sheet_matplotlib.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pydicom 3 | import numpy as np 4 | import cv2 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | 8 | 9 | # Function to apply high-contrast windowing to DICOM image pixel data 10 | def apply_high_contrast_windowing(pixels): 11 | # Set the minimum value to zero (black background) 12 | min_val = 0 13 | # Calculate the maximum value based on a certain percentile of the pixel intensity 14 | max_val = np.percentile(pixels, 95) # Adjust this percentile as needed to emphasize text 15 | 16 | # Apply the windowing 17 | windowed_pixels = np.clip(pixels, min_val, max_val) 18 | # Normalize to 0-255 range 19 | if max_val > min_val: # Avoid division by zero 20 | windowed_pixels = ((windowed_pixels - min_val) / (max_val - min_val)) * 255.0 21 | else: 22 | windowed_pixels = np.zeros_like(pixels) # If all pixels are the same, set to zero 23 | 24 | return windowed_pixels.astype(np.uint8) 25 | 26 | 27 | def load_dicom_image(filepath, target_size): 28 | ds = pydicom.dcmread(filepath) 29 | pixels = ds.pixel_array 30 | 31 | if ds.PhotometricInterpretation == "RGB": 32 | pixels = ds.pixel_array.astype(np.uint8) # Assume the data is already in RGB format 33 | else: 34 | # Apply windowing for grayscale images 35 | pixels = apply_high_contrast_windowing(ds.pixel_array) 36 | # pixels = np.stack((pixels,) * 3, axis=-1) # Convert to 3-channel RGB format 37 | 38 | # Resize using cv2 for better performance 39 | pixels = cv2.resize(pixels, target_size, interpolation=cv2.INTER_LINEAR) 40 | return pixels 41 | 42 | 43 | # Compile DICOM files from the given patient IDs 44 | def compile_dicom_files(patient_ids, base_dir): 45 | dicom_files = [] 46 | for patient_id in patient_ids: 47 | patient_path = os.path.join(base_dir, patient_id) 48 | for study_uid in os.listdir(patient_path): 49 | study_path = os.path.join(patient_path, study_uid) 50 | for series_uid in os.listdir(study_path): 51 | series_path = os.path.join(study_path, series_uid) 52 | for file in os.listdir(series_path): 53 | if file.endswith(".dcm"): 54 | dicom_files.append(os.path.join(series_path, file)) 55 | return dicom_files 56 | 57 | 58 | def display_contact_sheet(patient_ids, base_dir, image_size=(150, 150), columns=4): 59 | # Load DICOM files 60 | dicom_files = compile_dicom_files(patient_ids, base_dir) 61 | 62 | # Load images 63 | images = [load_dicom_image(filepath, target_size=image_size) for filepath in dicom_files] 64 | 65 | # Calculate number of rows 66 | rows = len(images) // columns + (1 if len(images) % columns != 0 else 0) 67 | 68 | # Create a figure for the contact sheet 69 | fig, axes = plt.subplots(rows, columns, figsize=(columns * 2, rows * 2), constrained_layout=True) 70 | axes = axes.flatten() # Flatten to easily index each subplot 71 | 72 | for i, ax in enumerate(axes): 73 | if i < len(images): 74 | ax.imshow(images[i]) 75 | ax.axis("off") 76 | else: 77 | ax.axis("off") # Hide any extra axes 78 | 79 | plt.show() 80 | -------------------------------------------------------------------------------- /src/prototyping/contact_sheet_scroll_frame.py: -------------------------------------------------------------------------------- 1 | import customtkinter as ctk 2 | from PIL import Image, ImageTk 3 | 4 | # Create the main window 5 | app = ctk.CTk() 6 | app.geometry("800x600") 7 | 8 | # Create a scrollable frame to hold the thumbnails 9 | scroll_frame = ctk.CTkScrollableFrame(app) 10 | scroll_frame.pack(fill="both", expand=True) 11 | 12 | # Example list of image paths 13 | image_paths = ["image1.jpg", "image2.jpg", "image3.jpg", "image4.jpg"] 14 | 15 | # Function to load thumbnails and display them in a grid 16 | thumbnails = [] 17 | 18 | 19 | def display_thumbnails(): 20 | global thumbnails # keep a reference to prevent garbage collection 21 | thumbnails = [] 22 | width = scroll_frame.winfo_width() 23 | thumbnail_size = 100 # Example thumbnail size 24 | columns = max(width // thumbnail_size, 1) # Calculate number of columns 25 | 26 | for i, img_path in enumerate(image_paths): 27 | image = Image.open(img_path).resize((thumbnail_size, thumbnail_size), Image.ANTIALIAS) 28 | photo = ImageTk.PhotoImage(image) 29 | thumbnails.append(photo) 30 | label = ctk.CTkLabel(scroll_frame, image=photo) 31 | row = i // columns 32 | col = i % columns 33 | label.grid(row=row, column=col, padx=5, pady=5, sticky="nsew") 34 | 35 | 36 | # Function to update the grid on resize 37 | def on_resize(event): 38 | display_thumbnails() 39 | 40 | 41 | # Bind the resize event to the function 42 | scroll_frame.bind("", on_resize) 43 | 44 | # Initial display of thumbnails 45 | display_thumbnails() 46 | 47 | app.mainloop() 48 | -------------------------------------------------------------------------------- /src/prototyping/ctk_toplevel.py: -------------------------------------------------------------------------------- 1 | import customtkinter as ctk 2 | 3 | def open_toplevel(): 4 | toplevel = ctk.CTkToplevel(root) # Set the 'top' attribute to the parent window 'root' 5 | toplevel.title("Toplevel Window") 6 | toplevel.geometry("300x200") 7 | toplevel.lift() 8 | 9 | root = ctk.CTk() 10 | root.title("Main Window") 11 | 12 | button = ctk.CTkButton(root, text="Open Toplevel", command=open_toplevel) 13 | button.pack() 14 | 15 | root.mainloop() 16 | -------------------------------------------------------------------------------- /src/prototyping/ctkinputdlg.py: -------------------------------------------------------------------------------- 1 | import customtkinter 2 | 3 | app = customtkinter.CTk() 4 | app.geometry("400x300") 5 | 6 | 7 | def button_click_event(): 8 | dialog = customtkinter.CTkInputDialog(text="Type in a number:", title="Test") 9 | print("Number:", dialog.get_input()) 10 | 11 | 12 | button = customtkinter.CTkButton(app, text="Open Dialog", command=button_click_event) 13 | button.pack(padx=20, pady=20) 14 | 15 | app.mainloop() 16 | -------------------------------------------------------------------------------- /src/prototyping/ctksliderwithmarkers.py: -------------------------------------------------------------------------------- 1 | class CTkSliderWithMarkers(ctk.CTkSlider): 2 | def __init__(self, master=None, marker_height=10, **kwargs): 3 | """ 4 | Custom slider with evenly spaced markers. 5 | 6 | Args: 7 | master: Parent widget. 8 | marker_height (int): Height of the markers. 9 | **kwargs: Additional arguments for CTkSlider. 10 | """ 11 | super().__init__(master, **kwargs) 12 | self.marker_height = marker_height 13 | 14 | # Use the slider's background color for the canvas 15 | slider_bg_color = self._apply_appearance_mode(self.cget("fg_color")) 16 | 17 | self.markers_canvas = ctk.CTkCanvas(self, bg=slider_bg_color, highlightthickness=0) 18 | self.markers_canvas.place(relx=0, rely=1, relwidth=1, y=-marker_height) 19 | 20 | self.bind("", self._draw_markers) 21 | 22 | def _draw_markers(self, event=None): 23 | """Draw evenly spaced markers below the slider.""" 24 | self.markers_canvas.delete("marker") # Clear previous markers 25 | width = self.markers_canvas.winfo_width() 26 | 27 | if self._number_of_steps is None or width == 1: 28 | return # Prevent errors when widget is initializing or number_of_steps is None 29 | 30 | step = width / (self._number_of_steps - 1) 31 | 32 | for i in range(self._number_of_steps): 33 | x = i * step 34 | self.markers_canvas.create_line( 35 | x, 0, x, self.marker_height, fill=self._apply_appearance_mode(self.cget("progress_color")), tag="marker" 36 | ) 37 | -------------------------------------------------------------------------------- /src/prototyping/dataclass_json.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import shutil 4 | import json 5 | import tempfile 6 | from logging import WARNING 7 | from anonymizer.model.project import ProjectModel, NetworkTimeouts, LoggingLevels 8 | from anonymizer.model.project import DICOMNode 9 | 10 | 11 | LocalSCU = DICOMNode("127.0.0.1", 0, "ANONYMIZER", True) 12 | LocalStorageSCP = DICOMNode("127.0.0.1", 1045, "ANONYMIZER", True) 13 | PACSSimulatorSCP = DICOMNode("127.0.0.1", 1046, "TESTPACS", False) 14 | OrthancSCP = DICOMNode("127.0.0.1", 4242, "ORTHANC", False) 15 | 16 | RemoteSCPDict: dict[str, DICOMNode] = { 17 | PACSSimulatorSCP.aet: PACSSimulatorSCP, 18 | OrthancSCP.aet: OrthancSCP, 19 | LocalStorageSCP.aet: LocalStorageSCP, 20 | } 21 | 22 | # Default project globals: 23 | TEST_SITEID = "99.99" 24 | TEST_PROJECTNAME = "ANONYMIZER_UNIT_TEST" 25 | TEST_UIDROOT = "1.2.826.0.1.3680043.10.474" 26 | 27 | anon_store = Path(tempfile.mkdtemp(), LocalSCU.aet) 28 | # Make sure storage directory exists: 29 | os.makedirs(anon_store, exist_ok=True) 30 | # Create Test ProjectModel: 31 | project_model = ProjectModel( 32 | site_id=TEST_SITEID, 33 | project_name=TEST_PROJECTNAME, 34 | uid_root=TEST_UIDROOT, 35 | remove_pixel_phi=False, 36 | storage_dir=anon_store, 37 | scu=LocalSCU, 38 | scp=LocalStorageSCP, 39 | remote_scps=RemoteSCPDict, 40 | network_timeouts=NetworkTimeouts(2, 5, 5, 15), 41 | anonymizer_script_path=Path("src/anonymizer/assets/scripts/default-anonymizer.script"), 42 | logging_levels=LoggingLevels(anonymizer=WARNING, pynetdicom=WARNING, pydicom=False), 43 | ) 44 | 45 | # Serialize to JSON 46 | json_data = project_model.to_json(indent=4) # Get JSON string 47 | print(json_data) 48 | 49 | # Deserialize from JSON 50 | project_model_from_json = ProjectModel.from_json(json_data) # Load from JSON string 51 | print(project_model_from_json) 52 | -------------------------------------------------------------------------------- /src/prototyping/epic_fhir_1.py: -------------------------------------------------------------------------------- 1 | from requests_oauthlib import OAuth2Session 2 | 3 | # https://fhir.jefferson.edu/FHIRProxy/api/FHIR/R4 4 | 5 | # Step 1: 6 | # https://fhir.epic.com/interconnect-fhir-oauth/oauth2/authorize?response_type=code&redirect_uri=[redirect_uri]&client_id=[client_id]&state=[state]&aud=[audience]&scope=[scope] 7 | 8 | EPIC_SANDBOX_BASE_URL = "https://fhir.epic.com/interconnect-fhir-oauth/" 9 | 10 | FHIR_API_R4_URL = EPIC_SANDBOX_BASE_URL + "api/FHIR/R4/" 11 | 12 | SANDBOX_CLIENT_ID = "19875d40-5c73-4a7f-9d0b-78015ca70f05" 13 | 14 | REDIRECT_URI = "127.0.0.1:8765/callback" 15 | 16 | authorize_url = EPIC_SANDBOX_BASE_URL + "authorize" # OAuth2 authorize endpoint 17 | token_url = EPIC_SANDBOX_BASE_URL + "token" # OAuth2 token endpoint 18 | 19 | # OAuth2 workflow: 20 | oauth = OAuth2Session(SANDBOX_CLIENT_ID, redirect_uri=REDIRECT_URI) 21 | 22 | # Step 1: Get authorization URL and redirect user to it 23 | authorization_url, state = oauth.authorization_url(authorize_url) 24 | 25 | print(f"Please go to this URL and authorize access: {authorization_url}") 26 | -------------------------------------------------------------------------------- /src/prototyping/epic_fhir_2.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | from urllib.parse import urlencode 3 | 4 | # Epic OAuth2 authorization endpoint 5 | authorize_url = "https://fhir.epic.com/interconnect-fhir-oauth/oauth2/authorize" 6 | 7 | # Client-specific details 8 | client_id = "19875d40-5c73-4a7f-9d0b-78015ca70f05" 9 | redirect_uri = "https://127.0.0.1:8765/callback" 10 | state = "random_state_value" # Optional but recommended 11 | scope = "openid fhirUser" # Required for Epic 12 | base_aud = "https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4/" # The base FHIR server URL 13 | 14 | # Additional parameters for PKCE (if required) 15 | code_challenge = None # Replace with your S256 hashed value if using PKCE 16 | code_challenge_method = "S256" # Optional, required if using code_challenge 17 | 18 | # Build the authorization request 19 | params = { 20 | "response_type": "code", 21 | "client_id": client_id, 22 | "redirect_uri": redirect_uri, 23 | "state": state, 24 | "scope": scope, 25 | "aud": base_aud, 26 | } 27 | 28 | if code_challenge: 29 | params.update({"code_challenge": code_challenge, "code_challenge_method": code_challenge_method}) 30 | 31 | # Construct the full URL 32 | url = f"{authorize_url}?{urlencode(params)}" 33 | 34 | # Open the URL in a browser for the user to authenticate 35 | print("Opening the browser to authenticate...") 36 | webbrowser.open(url) 37 | 38 | # Instructions for the user 39 | print("Once authenticated, you will be redirected to your callback URI.") 40 | print("Check the URL for a 'code' parameter and use it in the next step.") 41 | 42 | # Note: The callback URI handler is not implemented in this script. 43 | # You should set up a server or other mechanism to handle the redirect and capture the authorization code. 44 | -------------------------------------------------------------------------------- /src/prototyping/get_series_uids.py: -------------------------------------------------------------------------------- 1 | from pynetdicom import AE 2 | from pynetdicom.sop_class import StudyRootQueryRetrieveInformationModelFind 3 | 4 | 5 | 6 | # Create an Application Entity 7 | ae = AE("ANONYMIZER") 8 | 9 | # Create a C-FIND request for Series with the Study Instance UID 10 | request = ( 11 | (0x0008, 0x0052), 'SERIES', 12 | (0x0020, 0x000D), 'Your Study Instance UID' # Replace with the actual Study Instance UID 13 | ) 14 | 15 | study_root_sop_class = StudyRootQueryRetrieveInformationModelFind 16 | transfer_syntaxes = [ 17 | '1.2.840.10008.1.2', # Implicit VR Little Endian 18 | '1.2.840.10008.1.2.1', # Explicit VR Little Endian 19 | '1.2.840.10008.1.2.2' # Explicit VR Big Endian 20 | ] 21 | 22 | # Add the Presentation Context 23 | for ts in transfer_syntaxes: 24 | ae.add_supported_context(study_root_sop_class, ts) 25 | 26 | # Establish a connection to the PACS 27 | assoc = ae.associate('pacs_host', 11112) # Replace with your PACS host and port 28 | 29 | if assoc.is_established: 30 | # Send the C-FIND request 31 | responses = assoc.send_c_find(request) 32 | 33 | # Process the responses and collect the Series Instance UIDs 34 | series_uids = [response.SeriesInstanceUID for response in responses] 35 | 36 | # Close the association 37 | assoc.release() 38 | 39 | # Print the list of Series Instance UIDs 40 | for uid in series_uids: 41 | print(uid) 42 | else: 43 | print("Association failed") 44 | 45 | -------------------------------------------------------------------------------- /src/prototyping/hapi_fhir_2.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from pprint import pprint 3 | 4 | FHIR_BASE_URL = "http://hapi.fhir.org/baseR4" 5 | 6 | # HL7 (v6.02) Code System: V2-0074 Active as of 2019-12-01: https://terminology.hl7.org/6.0.2/CodeSystem-v2-0074.html 7 | # Basic query to fetch DiagnosticReports 8 | diagnostic_url = f"{FHIR_BASE_URL}/DiagnosticReport" 9 | params = { 10 | "category": "http://terminology.hl7.org/CodeSystem/v2-0074|RAD", # RAD stands for Radiology 11 | #'identifier:exists': 'true', # this doesn't work 12 | "status": "final", 13 | # 'category': 'http://terminology.hl7.org/CodeSystem/v2-0074|LAB', # Lab stands for Laboratory 14 | # 'category': 'http://terminology.hl7.org/CodeSystem/v2-0074|MB', # MB stands for Microbiology 15 | # 'category': 'http://terminology.hl7.org/CodeSystem/v2-0074|PAT', # PAT stands for Pathology 16 | "_count": 500, # there are 500 diagnostic reports in total on server 17 | } 18 | 19 | response = requests.get(diagnostic_url, params=params) 20 | 21 | if response.status_code != 200: 22 | print(f"Failed to fetch diagnostic reports. Status code: {response.status_code}") 23 | exit(1) 24 | 25 | reports = response.json() 26 | 27 | # Check if 'total' is present in the response 28 | total_reports = reports.get("total", len(reports.get("entry", []))) 29 | 30 | print(f"Total RADIOLOGY Diagnostic Reports Retrieved: {total_reports}") 31 | 32 | studies = 0 33 | 34 | for entry in reports.get("entry", []): 35 | report = entry["resource"] 36 | 37 | if ( 38 | "identifier" not in report or "contained" not in report 39 | ): # No PACS reference / Accession Number 40 | continue 41 | 42 | if "code" in report: 43 | print("***") 44 | pprint(report["code"]) 45 | else: 46 | print("No diagnostic code found in this report.") 47 | 48 | studies += 1 49 | 50 | if "presentedForm" in report: 51 | for form in report["presentedForm"]: 52 | if "data" in form: 53 | form["data"] = f'<{form['contentType']} encoded data>' 54 | 55 | pprint(report) 56 | 57 | 58 | print(f"Total Studies with identifiers: {studies}") 59 | -------------------------------------------------------------------------------- /src/prototyping/html_helloworld.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkhtmlview import HTMLLabel, HTMLScrolledText 3 | 4 | root = tk.Tk() 5 | html_label = HTMLScrolledText(root, html='

    Hello World

    ') 6 | html_label.pack(fill="both", expand=True) 7 | html_label.fit_height() 8 | root.mainloop() 9 | -------------------------------------------------------------------------------- /src/prototyping/html_instructions.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkhtmlview import RenderHTML, HTMLScrolledText 3 | 4 | root = tk.Tk() 5 | html_label = HTMLScrolledText( 6 | root, width=130, height=60, padx=10, pady=10, html=RenderHTML("assets/locales/en_US/html/images/overview.html") 7 | ) 8 | html_label.pack(fill="both", expand=True) 9 | root.mainloop() 10 | -------------------------------------------------------------------------------- /src/prototyping/imageviewer_1_ctk.py: -------------------------------------------------------------------------------- 1 | import customtkinter as ctk 2 | 3 | 4 | def key_pressed(event): 5 | print(f"Key pressed: {event.keysym}") 6 | print(f"Widget with focus: {root.focus_get()}") # Add this line 7 | 8 | 9 | root = ctk.CTk() 10 | root.geometry("200x100") 11 | 12 | frame = ctk.CTkFrame(root, width=100, height=50) 13 | frame.pack(expand=True, fill="both") 14 | 15 | # Set focus to the frame *after* packing it 16 | frame.focus_set() 17 | 18 | # Bind the event to the frame. "" is a catch-all for key presses. 19 | frame.bind("", key_pressed) 20 | 21 | # Add a label to the frame (to make sure it's not completely empty) 22 | label = ctk.CTkLabel(frame, text="Click here, then press keys") 23 | label.pack(pady=20) 24 | 25 | 26 | # Add a click handler to ensure focus is set: 27 | def frame_clicked(event): 28 | # see here: https://stackoverflow.com/questions/77676235/tkinter-focus-set-on-frame 29 | # The reason why the customtkinter code doesn't work is that customtkinter has some peculiarities in how it handles bindings. ' 30 | # 'It unfortunately overrides bind to apply any bindings to a canvas widget embedded in the frame rather than the frame itself. ' 31 | # 'So, the bindings get added to a canvas, but you set focus to the frame. 32 | frame._canvas.focus_set() 33 | print("Frame clicked, focus set.") 34 | 35 | 36 | frame.bind("", frame_clicked) 37 | 38 | root.mainloop() 39 | -------------------------------------------------------------------------------- /src/prototyping/imageviewer_2_tk.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | 4 | def key_pressed(event): 5 | print(f"Key pressed: {event.keysym}") 6 | 7 | 8 | root = tk.Tk() 9 | root.geometry("200x100") 10 | 11 | frame = tk.Frame(root, width=100, height=50, bg="red") 12 | frame.pack(expand=True, fill="both") 13 | frame.focus_set() # Give the frame focus 14 | 15 | frame.bind("", key_pressed) # Bind to the frame 16 | 17 | root.mainloop() 18 | -------------------------------------------------------------------------------- /src/prototyping/lazy_load_image_scroll_tk.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from PIL import Image, ImageTk 4 | 5 | 6 | class LazyLoadImageScrollApp(tk.Tk): 7 | def __init__(self): 8 | super().__init__() 9 | self.title("Lazy Load Image Scroll") 10 | self.geometry("300x500") 11 | 12 | # Canvas and Scrollbar Setup 13 | self.canvas = tk.Canvas(self, width=300, height=500) 14 | self.scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview) 15 | self.scrollable_frame = tk.Frame(self.canvas) 16 | 17 | # Create a scrollable window in the canvas 18 | self.scrollable_frame_id = self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") 19 | self.canvas.configure(yscrollcommand=self.scrollbar.set) 20 | 21 | # Pack the canvas and scrollbar 22 | self.canvas.pack(side="left", fill="both", expand=True) 23 | self.scrollbar.pack(side="right", fill="y") 24 | 25 | # Bind events for resizing and scrolling 26 | self.scrollable_frame.bind("", self.update_scrollregion) 27 | self.canvas.bind("", self.update_frame_width) 28 | self.canvas.bind("", self.mouse_scroll) 29 | 30 | # Generate dummy images for lazy loading 31 | self.images = [] 32 | for i in range(100): 33 | img = Image.new("RGB", (200, 100), (i * 2 % 256, 100, 255 - i * 2 % 256)) 34 | self.images.append(img) 35 | 36 | # Store visible widgets and range 37 | self.image_labels = {} 38 | self.visible_range = (0, 0) 39 | 40 | # Initial rendering 41 | self.update_visible() 42 | 43 | def update_scrollregion(self, event=None): 44 | """Update the scroll region of the canvas.""" 45 | self.canvas.configure(scrollregion=self.canvas.bbox("all")) 46 | 47 | def update_frame_width(self, event=None): 48 | """Ensure the frame width matches the canvas width.""" 49 | canvas_width = self.canvas.winfo_width() 50 | self.canvas.itemconfig(self.scrollable_frame_id, width=canvas_width) 51 | 52 | def mouse_scroll(self, event): 53 | """Enable mouse wheel scrolling.""" 54 | self.canvas.yview_scroll(-1 * int(event.delta / 120), "units") 55 | self.update_visible() 56 | 57 | def update_visible(self): 58 | """Dynamically update visible images in the scrollable frame.""" 59 | canvas_top = self.canvas.canvasy(0) 60 | canvas_bottom = self.canvas.canvasy(self.canvas.winfo_height()) 61 | 62 | img_height = 100 # Each image's height 63 | start_idx = max(0, int(canvas_top // img_height)) 64 | end_idx = min(len(self.images), int(canvas_bottom // img_height) + 1) 65 | 66 | if self.visible_range == (start_idx, end_idx): 67 | return # Range hasn't changed; no need to update 68 | self.visible_range = (start_idx, end_idx) 69 | 70 | # Add images that should be visible 71 | for i in range(start_idx, end_idx): 72 | if i not in self.image_labels: 73 | img = ImageTk.PhotoImage(self.images[i]) 74 | label = tk.Label(self.scrollable_frame, image=img) 75 | label.image = img # Keep reference to avoid garbage collection 76 | label.grid(row=i, column=0, pady=5) 77 | self.image_labels[i] = label 78 | 79 | # Remove images that are now off-screen 80 | for i in list(self.image_labels.keys()): 81 | if i < start_idx or i >= end_idx: 82 | self.image_labels[i].destroy() 83 | del self.image_labels[i] 84 | 85 | 86 | if __name__ == "__main__": 87 | app = LazyLoadImageScrollApp() 88 | app.mainloop() 89 | -------------------------------------------------------------------------------- /src/prototyping/locale.py: -------------------------------------------------------------------------------- 1 | import locale 2 | 3 | locale.setlocale(locale.LC_ALL, "de_DE.UTF-8") 4 | for key, value in locale.localeconv().items(): 5 | print("%s: %s" % (key, value)) 6 | -------------------------------------------------------------------------------- /src/prototyping/matplotlib_3d.py: -------------------------------------------------------------------------------- 1 | import pydicom 2 | import os 3 | import numpy as np 4 | 5 | # pipenv matplotlib to test this 6 | import matplotlib.pyplot as plt # type: ignore 7 | from mpl_toolkits.mplot3d import Axes3D # type: ignore 8 | 9 | # Directory containing DICOM files of the CT scan 10 | directory = "path_to_dicom_files_directory" 11 | 12 | # Load DICOM files and extract pixel data 13 | slices = [] 14 | for filename in sorted(os.listdir(directory)): 15 | if filename.endswith(".dcm"): 16 | ds = pydicom.dcmread(os.path.join(directory, filename)) 17 | slices.append(ds.pixel_array) 18 | 19 | # Convert the list of 2D arrays into a 3D NumPy array 20 | volume = np.stack(slices, axis=0) 21 | 22 | # Plot the 3D volume 23 | fig = plt.figure() 24 | ax = fig.add_subplot(111, projection="3d") 25 | ax.voxels(volume, edgecolor="k") 26 | ax.set_xlabel("X") 27 | ax.set_ylabel("Y") 28 | ax.set_zlabel("Z") 29 | plt.show() 30 | -------------------------------------------------------------------------------- /src/prototyping/modalityLUT_smpte_test_pattern.py: -------------------------------------------------------------------------------- 1 | import pydicom 2 | 3 | # pipenv matplotlib to test this 4 | import matplotlib.pyplot as plt # type: ignore 5 | 6 | # Load the DICOM file 7 | ds = pydicom.dcmread(pydicom.data.get_testdata_file("mlut_18.dcm")) 8 | 9 | # Access the pixel data 10 | pixel_data = ds.pixel_array 11 | 12 | # Plot the image 13 | plt.imshow(pixel_data, cmap="gray") 14 | plt.axis("off") # Hide axes 15 | plt.show() 16 | -------------------------------------------------------------------------------- /src/prototyping/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/prototyping/pause.png -------------------------------------------------------------------------------- /src/prototyping/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/src/prototyping/play.png -------------------------------------------------------------------------------- /src/prototyping/progress_dialog.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union 2 | from queue import Queue 3 | import tkinter as tk 4 | import customtkinter as ctk 5 | from tkinter import ttk 6 | import logging 7 | from utils.translate import _ 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class ProgressDialog(tk.Toplevel): 13 | # class ProgressDialog(ctk.CTkToplevel): 14 | progress_update_interval = 300 15 | 16 | def __init__( 17 | self, 18 | parent, 19 | Q_to_monitor: Queue, 20 | title: str = _("Progress Dialog"), 21 | sub_title: str = _("Please wait..."), 22 | ): 23 | super().__init__(master=parent) 24 | self._Q_to_monitor = Q_to_monitor 25 | # latch items in queue for progress bar max value 26 | self._maxQ = Q_to_monitor.qsize() 27 | if self._maxQ == 0: 28 | self._maxQ = 1 29 | self.title(title) 30 | self._sub_title = sub_title 31 | self.attributes("-topmost", True) # stay on top 32 | self.protocol("WM_DELETE_WINDOW", self._on_cancel) 33 | self.grab_set() # make dialog modal 34 | self.resizable(False, False) 35 | self._user_input: Union[list, None] = None 36 | self.rowconfigure(0, weight=1) 37 | self.columnconfigure(1, weight=1) 38 | self._create_widgets() 39 | self.bind("", self._escape_keypress) 40 | self._update_progress() 41 | 42 | def _create_widgets(self): 43 | logger.info(f"_create_widgets") 44 | PAD = 10 45 | 46 | self._sub_title_label = ctk.CTkLabel(self, text=_(self._sub_title)) 47 | self._sub_title_label.grid(row=0, column=0, padx=PAD, pady=PAD, sticky="w") 48 | 49 | self._progressbar = ctk.CTkProgressBar(self) 50 | self._progressbar.grid( 51 | row=1, 52 | column=0, 53 | padx=(PAD, 2 * PAD), 54 | pady=(PAD, 0), 55 | sticky="ew", 56 | ) 57 | 58 | self._progressbar.set(0) 59 | 60 | self._progress_label = ctk.CTkLabel(self, text=f"Process 0 of {self._maxQ}") 61 | self._progress_label.grid(row=2, column=0, padx=PAD, pady=(0, PAD), sticky="w") 62 | 63 | self._cancel_button = ctk.CTkButton(self, text=_("Cancel"), command=self._on_cancel) 64 | self._cancel_button.grid( 65 | row=3, 66 | column=0, 67 | padx=PAD, 68 | pady=(0, PAD), 69 | sticky="e", 70 | ) 71 | 72 | def _update_progress(self): 73 | self._last_qsize = self._Q_to_monitor.qsize() 74 | 75 | if self._last_qsize == 0: 76 | logger.info(f"Q is empty, progress bar exit") 77 | self._progressbar.set(1) 78 | self.grab_release() 79 | self.destroy() 80 | return 81 | 82 | current_ndx = self._maxQ - self._last_qsize 83 | self._progressbar.set(current_ndx / self._maxQ) 84 | self._progress_label.configure(text=f"Processing {current_ndx} of {self._maxQ}") 85 | self.after(self.progress_update_interval, self._update_progress) 86 | 87 | def _escape_keypress(self, event): 88 | logger.info(f"_escape_pressed") 89 | self._on_cancel() 90 | 91 | def _on_cancel(self): 92 | logger.info(f"_on_cancel {self._Q_to_monitor.qsize()} remain in Q, clearing Q...") 93 | # dump all items in queue to clear it 94 | while not self._Q_to_monitor.empty(): 95 | self._Q_to_monitor.get() 96 | self.grab_release() 97 | self.destroy() 98 | 99 | def get_input(self): 100 | self.focus() 101 | self.master.wait_window(self) 102 | return self._last_qsize 103 | -------------------------------------------------------------------------------- /src/prototyping/pynetdicom_scp.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pydicom.filewriter import write_file_meta_info 3 | from pynetdicom.ae import ApplicationEntity as AE 4 | from pynetdicom.events import Event, EVT_C_STORE, EVT_C_ECHO 5 | from pynetdicom import debug_logger 6 | from pynetdicom.presentation import ( 7 | build_context, 8 | AllStoragePresentationContexts, 9 | VerificationPresentationContexts, 10 | ) 11 | from pynetdicom._globals import ALL_TRANSFER_SYNTAXES 12 | 13 | from utils.storage import local_storage_path 14 | 15 | debug_logger() 16 | 17 | 18 | def handle_store(event, storage_dir): 19 | try: 20 | os.makedirs(storage_dir, exist_ok=True) 21 | except: 22 | return 0xC001 23 | 24 | # os.path.join(storage_dir, event.request.AffectedSOPInstanceUID) 25 | 26 | remote = event.assoc.remote 27 | ds = event.dataset 28 | ds.file_meta = event.file_meta 29 | fname = local_storage_path(storage_dir, ds) 30 | ds.save_as(fname, write_like_original=False) 31 | 32 | # with open(fname, "wb") as f: 33 | # # Write the preamble, prefix and file meta information elements 34 | # f.write(b"\x00" * 128) 35 | # f.write(b"DICM") 36 | # write_file_meta_info(f, event.file_meta) 37 | # # Write the raw encoded dataset 38 | # f.write(event.request.DataSet.getvalue()) 39 | 40 | return 0x0000 41 | 42 | 43 | handlers = [ 44 | ( 45 | EVT_C_STORE, 46 | handle_store, 47 | ["/Users/michaelevans/Downloads/RSNA_ANON_STORE/PYTHON_ANON_STORE"], 48 | ) 49 | ] 50 | install_dir = os.path.dirname(os.path.realpath(__file__)) 51 | os.chdir(install_dir) 52 | ae = AE("ANONSTORE") 53 | storage_sop_classes = [ 54 | cx.abstract_syntax 55 | for cx in AllStoragePresentationContexts + VerificationPresentationContexts 56 | ] 57 | for uid in storage_sop_classes: 58 | ae.add_supported_context(uid, ALL_TRANSFER_SYNTAXES) # type: ignore 59 | 60 | ae.start_server(("127.0.0.1", 1045), block=True, evt_handlers=handlers) # type: ignore 61 | -------------------------------------------------------------------------------- /src/prototyping/radon_raw_totals.py: -------------------------------------------------------------------------------- 1 | # run: radon raw -i "tests,docs,src/anonymizer/prototyping" . > radon_results.txt 2 | # then run this script to get totals 3 | from pprint import pprint 4 | 5 | totals = { 6 | "LOC": 0, # Lines of Code 7 | "LLOC": 0, # Logical Lines of Code 8 | "SLOC": 0, # Source Lines of Code 9 | "Comments": 0, # Number of comment lines 10 | "Multi": 0, # Number of lines with multi-line strings 11 | "Blank": 0, # Number of blank lines 12 | "Single comments": 0, # Number of single-line comments 13 | } 14 | 15 | with open("./radon_results.txt", "r") as file: 16 | lines = file.readlines() 17 | for line in lines: 18 | line = line.strip() # Remove whitespace 19 | for metric in totals: 20 | if line.startswith(metric + ":"): 21 | value = int(line.split(":")[1].strip()) 22 | totals[metric] += value 23 | 24 | pprint(totals, sort_dicts=False) 25 | -------------------------------------------------------------------------------- /src/prototyping/set_gha_env.py: -------------------------------------------------------------------------------- 1 | # Run from github actions to set the version in the environment file to be accessed from build.yml release step 2 | 3 | import os 4 | import importlib.metadata 5 | 6 | env_file = os.getenv("GITHUB_ENV") 7 | 8 | if env_file is not None: 9 | with open(env_file, "a", encoding="utf-8") as f: 10 | f.write(f"version={importlib.metadata.version("anonymizer")}") 11 | -------------------------------------------------------------------------------- /src/prototyping/storage_dir.py: -------------------------------------------------------------------------------- 1 | import customtkinter as ctk 2 | from tkinter import filedialog 3 | import logging 4 | import prototyping.config as config 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | # Initialize storage directory 9 | storage_directory = "" 10 | 11 | 12 | def get_storage_directory(): 13 | return storage_directory 14 | 15 | 16 | # Load module globals from config.json 17 | settings = config.load(__name__) 18 | globals().update(settings) 19 | 20 | 21 | def open_directory_dialog(label: ctk.CTkLabel): 22 | global storage_directory 23 | path = filedialog.askdirectory() 24 | if path: 25 | storage_directory = path 26 | label.configure(text=storage_directory) 27 | config.save(__name__, "storage_directory", storage_directory) 28 | 29 | 30 | def create_view(view: ctk.CTkFrame, name: str): 31 | PAD = 10 32 | logger.info(f"Creating {name} View") 33 | 34 | button = ctk.CTkButton( 35 | view, 36 | text=name, 37 | command=lambda: open_directory_dialog(storage_directory_label), 38 | ) 39 | button.grid(row=0, column=0, pady=PAD, sticky="nw") 40 | 41 | storage_directory_label = ctk.CTkLabel(view, text=storage_directory) 42 | storage_directory_label.grid(row=0, column=1, pady=PAD, padx=PAD, sticky="nw") 43 | -------------------------------------------------------------------------------- /src/prototyping/test_translation.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | 3 | # Load the compiled MO file 4 | domain = "messages" 5 | localedir = "src/assets/locales" 6 | lang = "es" 7 | lang_translations = gettext.translation(domain, localedir, languages=[lang]) 8 | lang_translations.install() 9 | _ = lang_translations.gettext 10 | 11 | # Example usage: 12 | print(_("RSNA DICOM Anonymizer Version")) 13 | print(_("Config file not found: ")) 14 | # Continue printing other translations as needed 15 | -------------------------------------------------------------------------------- /src/prototyping/threadpooldemo.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import time 3 | import random 4 | 5 | 6 | # Define the worker function: 7 | def worker(task_num): 8 | duration = 5 # random.randint(1, 5) # Random duration between 1 to 5 seconds 9 | print(f"Task {task_num} starts, will run for {duration} seconds...") 10 | time.sleep(duration) 11 | print(f"Task {task_num} finished after {duration} seconds!") 12 | return f"Task {task_num} result" 13 | 14 | 15 | # Main execution: 16 | if __name__ == "__main__": 17 | NUM_TASKS = 10 # Number of tasks to run 18 | MAX_WORKERS = 3 # Maximum number of concurrent tasks 19 | 20 | # Using ThreadPoolExecutor ###THIS BLOCKS###: 21 | with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: 22 | # Submit tasks to executor: 23 | futures = [executor.submit(worker, i) for i in range(NUM_TASKS)] 24 | 25 | # Collect results: 26 | for future in concurrent.futures.as_completed(futures): 27 | print(future.result()) 28 | 29 | print("All tasks completed!") 30 | -------------------------------------------------------------------------------- /src/prototyping/tkcalendar.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from tkcalendar import Calendar, DateEntry 4 | 5 | 6 | def example1(): 7 | def print_sel(): 8 | print(cal.selection_get()) 9 | 10 | top = tk.Toplevel(root) 11 | 12 | cal = Calendar( 13 | top, 14 | font="Arial 14", 15 | selectmode="day", 16 | locale="en_US", 17 | cursor="hand1", 18 | year=2018, 19 | month=2, 20 | day=5, 21 | ) 22 | 23 | cal.pack(fill="both", expand=True) 24 | ttk.Button(top, text="ok", command=print_sel).pack() 25 | 26 | 27 | def example2(): 28 | top = tk.Toplevel(root) 29 | 30 | cal = Calendar(top, selectmode="none") 31 | date = cal.datetime.today() + cal.timedelta(days=2) 32 | cal.calevent_create(date, "Hello World", "message") 33 | cal.calevent_create(date, "Reminder 2", "reminder") 34 | cal.calevent_create(date + cal.timedelta(days=-2), "Reminder 1", "reminder") 35 | cal.calevent_create(date + cal.timedelta(days=3), "Message", "message") 36 | 37 | cal.tag_config("reminder", background="red", foreground="yellow") 38 | 39 | cal.pack(fill="both", expand=True) 40 | ttk.Label(top, text="Hover over the events.").pack() 41 | 42 | 43 | def example3(): 44 | top = tk.Toplevel(root) 45 | 46 | ttk.Label(top, text="Choose date").pack(padx=10, pady=10) 47 | 48 | cal = DateEntry( 49 | top, 50 | width=12, 51 | background="darkblue", 52 | foreground="white", 53 | borderwidth=2, 54 | year=2010, 55 | ) 56 | cal.pack(padx=10, pady=10) 57 | 58 | 59 | root = tk.Tk() 60 | ttk.Button(root, text="Calendar", command=example1).pack(padx=10, pady=10) 61 | ttk.Button(root, text="Calendar with events", command=example2).pack(padx=10, pady=10) 62 | ttk.Button(root, text="DateEntry", command=example3).pack(padx=10, pady=10) 63 | 64 | root.mainloop() 65 | -------------------------------------------------------------------------------- /src/prototyping/tkinter_toplevel.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | def open_toplevel(): 4 | toplevel = tk.Toplevel(root) # Set the 'top' attribute to the parent window 'root' 5 | toplevel.title("Toplevel Window") 6 | toplevel.geometry("300x200") 7 | 8 | root = tk.Tk() 9 | root.title("Main Window") 10 | 11 | button = tk.Button(root, text="Open Toplevel", command=open_toplevel) 12 | button.pack() 13 | 14 | root.mainloop() 15 | -------------------------------------------------------------------------------- /src/prototyping/tkmenutest1.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | 4 | def open_file(): 5 | print("Open file") 6 | 7 | 8 | def save_file(): 9 | print("Save file") 10 | 11 | 12 | def quit_app(): 13 | root.quit() 14 | 15 | 16 | root = tk.Tk() 17 | root.geometry("500x300") 18 | 19 | root.title("tkMenuTest") 20 | 21 | menu_bar = tk.Menu(root) 22 | 23 | file_menu = tk.Menu(menu_bar, tearoff=0) 24 | file_menu.add_command(label="Open", command=open_file) 25 | file_menu.add_command(label="Save", command=save_file) 26 | file_menu.add_separator() 27 | file_menu.add_command(label="Quit", command=quit_app) 28 | 29 | menu_bar.add_cascade(label="File", menu=file_menu) 30 | 31 | root.config(menu=menu_bar) 32 | 33 | root.mainloop() 34 | -------------------------------------------------------------------------------- /src/prototyping/tkmenutest2.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import filedialog 3 | 4 | 5 | class App: 6 | def __init__(self, root): 7 | self.root = root 8 | self.root.title("Tkinter Cascaded Menu Example") 9 | 10 | # Create a menu bar 11 | menubar = tk.Menu(root) 12 | root.config(menu=menubar) 13 | 14 | # File menu 15 | file_menu = tk.Menu(menubar, tearoff=0) 16 | menubar.add_cascade(label="File", menu=file_menu) 17 | 18 | # Open Recent menu (cascaded) 19 | open_recent_menu = tk.Menu(file_menu, tearoff=0) 20 | file_menu.add_cascade(label="Open Recent", menu=open_recent_menu) 21 | 22 | # Add sample directories to the Open Recent menu 23 | recent_directories = ["/path/to/dir1", "/path/to/dir2", "/path/to/dir3"] 24 | for directory in recent_directories: 25 | open_recent_menu.add_command( 26 | label=directory, command=lambda dir=directory: self.open_directory(dir) 27 | ) 28 | 29 | # Add a separator and "Exit" option to the File menu 30 | file_menu.add_separator() 31 | file_menu.add_command(label="Exit", command=root.destroy) 32 | 33 | def open_directory(self, directory): 34 | # Replace this function with the desired action for opening a directory 35 | print(f"Opening directory: {directory}") 36 | 37 | 38 | if __name__ == "__main__": 39 | root = tk.Tk() 40 | app = App(root) 41 | root.mainloop() 42 | -------------------------------------------------------------------------------- /src/prototyping/tkmenutest3.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import messagebox 3 | 4 | 5 | class DynamicMenuApp: 6 | def __init__(self, root): 7 | self.root = root 8 | self.root.title("Dynamic Menu Example") 9 | 10 | # Create the main menu 11 | self.menu = tk.Menu(root) 12 | root.config(menu=self.menu) 13 | 14 | # Create a File menu with initial items 15 | self.file_menu = tk.Menu(self.menu, tearoff=0) 16 | self.file_menu.add_command(label="Open", command=self.open_file) 17 | self.file_menu.add_command(label="Save", command=self.save_file) 18 | 19 | # Add the File menu to the main menu 20 | self.menu.add_cascade(label="File", menu=self.file_menu) 21 | 22 | # Create a button to toggle the menu state 23 | self.toggle_button = tk.Button(root, text="Toggle Menu State", command=self.toggle_menu_state) 24 | self.toggle_button.pack(pady=20) 25 | 26 | # Variable to track menu state 27 | self.menu_state = True # True means state with submenu 28 | 29 | # Create a submenu 30 | self.sub_menu = tk.Menu(self.file_menu, tearoff=0) 31 | self.sub_menu.add_command(label="Submenu Item 1", command=self.submenu_action) 32 | self.file_menu.add_cascade(label="Submenu", menu=self.sub_menu) 33 | 34 | def open_file(self): 35 | messagebox.showinfo("Open", "Open File clicked") 36 | 37 | def save_file(self): 38 | messagebox.showinfo("Save", "Save File clicked") 39 | 40 | def submenu_action(self): 41 | messagebox.showinfo("Submenu", "Submenu Item clicked") 42 | 43 | def toggle_menu_state(self): 44 | if self.menu_state: 45 | # Remove the submenu 46 | self.file_menu.delete("Submenu") 47 | self.menu_state = False 48 | else: 49 | # Re-add the submenu 50 | self.file_menu.add_cascade(label="Submenu", menu=self.sub_menu) 51 | self.menu_state = True 52 | 53 | 54 | if __name__ == "__main__": 55 | root = tk.Tk() 56 | app = DynamicMenuApp(root) 57 | root.mainloop() 58 | -------------------------------------------------------------------------------- /src/prototyping/treeviewtable_eg.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | # pipenv pandas to test this 4 | import pandas as pd # type: ignore 5 | import random 6 | import string 7 | 8 | # Generate more data for your DataFrame 9 | data = { 10 | "Name": [ 11 | "".join(random.choices(string.ascii_uppercase + string.ascii_lowercase, k=5)) 12 | for _ in range(20) 13 | ], 14 | "City": [ 15 | "".join(random.choices(string.ascii_uppercase + string.ascii_lowercase, k=7)) 16 | for _ in range(20) 17 | ], 18 | } 19 | df = pd.DataFrame(data) 20 | 21 | # Create the root window 22 | root = tk.Tk() 23 | root.title("DataFrame Display") 24 | 25 | # Create a Frame to hold the Treeview and Scrollbar 26 | frame = ttk.Frame(root) 27 | frame.pack(fill="both", expand=True) 28 | 29 | # Create the Treeview 30 | tree = ttk.Treeview(frame, columns=list(df.columns), show="headings") 31 | 32 | # Set up the column headers 33 | for col in df.columns: 34 | tree.heading(col, text=col, command=lambda _col=col: sortby(tree, _col, 0)) 35 | 36 | # Load the data into the Treeview 37 | for index, row in df.iterrows(): 38 | tree.insert("", "end", values=list(row)) 39 | 40 | # Create a Scrollbar and associate it with the Treeview 41 | scrollbar = ttk.Scrollbar(frame, orient="vertical", command=tree.yview) 42 | scrollbar.pack(side="right", fill="y") 43 | tree.configure(yscrollcommand=scrollbar.set) 44 | 45 | 46 | # Sorting function 47 | def sortby(tree, col, descending): 48 | data = [(tree.set(child, col), child) for child in tree.get_children("")] 49 | data.sort(reverse=descending) 50 | 51 | for ix, item in enumerate(data): 52 | tree.move(item[1], "", ix) 53 | 54 | # switch the heading so it will sort in the opposite direction 55 | tree.heading(col, command=lambda _col=col: sortby(tree, _col, int(not descending))) 56 | 57 | global sorted_column 58 | sorted_column = (col, descending) 59 | 60 | # Update the headers to indicate the sort column and direction 61 | for _col in df.columns: 62 | if _col == col: 63 | direction = " \u2193" if descending else " \u2191" # Down and up arrows 64 | tree.heading(_col, text=_col + direction) 65 | else: 66 | tree.heading(_col, text=_col) 67 | 68 | 69 | # Pack the Treeview 70 | tree.pack(fill="both", expand=True) 71 | 72 | # Initialize the sorted_column variable 73 | sorted_column = (df.columns[0], 0) 74 | 75 | # Sort the Treeview on the first column 76 | sortby(tree, df.columns[0], 0) 77 | 78 | root.mainloop() 79 | -------------------------------------------------------------------------------- /src/prototyping/ttktreeview.py: -------------------------------------------------------------------------------- 1 | from tkinter import ttk 2 | import customtkinter as ctk 3 | 4 | # Create the main window 5 | root = ctk.CTk() 6 | root.title("Treeview with Scrollbars") 7 | root.geometry("800x400") # Set the window size 8 | 9 | # Configure the grid to expand with the window 10 | root.grid_rowconfigure(0, weight=1) 11 | root.grid_columnconfigure(0, weight=1) 12 | 13 | # Create a frame to hold the Treeview and the scrollbars 14 | frame = ctk.CTkFrame(root) 15 | frame.grid(row=0, column=0, sticky="nsew") 16 | # Configure the frame to expand with the window 17 | frame.grid_rowconfigure(0, weight=1) 18 | frame.grid_columnconfigure(0, weight=1) 19 | 20 | 21 | # Create the Treeview widget 22 | columns = ("col1", "col2", "col3", "col4", "col5") 23 | tree = ttk.Treeview(frame, columns=columns, show="headings") 24 | 25 | # Define the column headings 26 | for col in columns: 27 | tree.heading(col, text=col.capitalize()) 28 | tree.column(col, width=100, stretch=False) 29 | 30 | # Adjust the width of the last column 31 | tree.column("col5", width=2000, stretch=False) 32 | 33 | # Create the vertical scrollbar 34 | vsb = ttk.Scrollbar(frame, orient="vertical", command=tree.yview) 35 | vsb.grid(row=0, column=1, sticky="ns") 36 | 37 | # Create the horizontal scrollbar 38 | hsb = ttk.Scrollbar(frame, orient="horizontal", command=tree.xview) 39 | hsb.grid(row=1, column=0, sticky="ew") 40 | 41 | # Configure the Treeview to use the scrollbars 42 | tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) 43 | 44 | # Place the Treeview in the grid 45 | tree.grid(row=0, column=0, sticky="nsew") 46 | 47 | # Add some test data, including one very wide item in the last column 48 | for i in range(100): 49 | if i == 50: 50 | # Insert a long string into the last column to ensure horizontal scrolling is required 51 | tree.insert( 52 | "", 53 | "end", 54 | values=( 55 | f"Item {i+1}-1", 56 | f"Item {i+1}-2", 57 | f"Item {i+1}-3", 58 | f"Item {i+1}-4", 59 | "A very long string that exceeds the column width and should require horizontal scrolling to view completely. This is to ensure that the horizontal scrollbar appears correctly.", 60 | ), 61 | ) 62 | else: 63 | tree.insert( 64 | "", "end", values=(f"Item {i+1}-1", f"Item {i+1}-2", f"Item {i+1}-3", f"Item {i+1}-4", f"Item {i+1}-5") 65 | ) 66 | 67 | # Start the Tkinter main loop 68 | root.mainloop() 69 | -------------------------------------------------------------------------------- /src/prototyping/ttktreeview2.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | # Function to toggle child visibility 6 | def toggle_children(item_id): 7 | # If children are already present, remove them 8 | children = tree.get_children(item_id) 9 | if children: 10 | tree.delete(children) 11 | else: 12 | # Add child item with embedded string 13 | tree.insert(item_id, "end", text="Embedded String: This is additional information") 14 | 15 | 16 | # Create the main window 17 | root = tk.Tk() 18 | root.title("Treeview with Expandable Rows") 19 | 20 | # Create a Treeview widget 21 | tree = ttk.Treeview(root) 22 | tree.pack(fill="both", expand=True) 23 | 24 | # Define columns and headings 25 | tree["columns"] = "details" 26 | tree.column("#0", width=150) 27 | tree.heading("#0", text="Item") 28 | tree.column("details", width=200) 29 | tree.heading("details", text="Details") 30 | 31 | # Insert root level items 32 | item1 = tree.insert("", "end", text="Item 1", values=("Details for Item 1")) 33 | item2 = tree.insert("", "end", text="Item 2", values=("Details for Item 2")) 34 | item3 = tree.insert("", "end", text="Item 3", values=("Details for Item 3")) 35 | 36 | # Insert a child item under "Item 3" 37 | child_item3 = tree.insert(item3, "end", text="Embedded String: This is additional information") 38 | tree.item(child_item3, open=True) # Open the child item initially 39 | 40 | # Bind double-click to toggle child visibility 41 | tree.bind("", lambda event: toggle_children(tree.selection()[0])) 42 | 43 | # Start the Tkinter main loop 44 | root.mainloop() 45 | -------------------------------------------------------------------------------- /src/prototyping/ttktreeview_popup.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | 4 | 5 | def show_popup(message): 6 | """Create and display a pop-up window with the given message""" 7 | popup = tk.Toplevel() 8 | popup.wm_title("Error Message") 9 | label = tk.Label(popup, text=message, wraplength=300, justify="left") 10 | label.pack(side="top", fill="x", pady=10, padx=10) 11 | button = tk.Button(popup, text="Close", command=popup.destroy) 12 | button.pack(pady=5) 13 | popup.geometry("400x200") 14 | 15 | 16 | def on_item_double_click(event): 17 | """Display the error message in a pop-up window on double-click""" 18 | item = tree.identify_row(event.y) 19 | if item: 20 | message = tree.item(item, "values")[0] 21 | show_popup(message) 22 | 23 | 24 | # Create the main window 25 | root = tk.Tk() 26 | root.title("Treeview with Error Messages") 27 | 28 | # Create a Treeview widget 29 | tree = ttk.Treeview(root) 30 | tree.pack(fill="both", expand=True) 31 | 32 | # Define columns and headings 33 | tree["columns"] = ("details",) 34 | tree.column("#0", width=150) 35 | tree.heading("#0", text="Item") 36 | tree.column("details", width=200) 37 | tree.heading("details", text="Details") 38 | 39 | # Example data with error messages 40 | data = [ 41 | ("Item 1", "Short error message."), 42 | ( 43 | "Item 2", 44 | "This is a much longer error message that might not fit in the column and should be visible in the popup.", 45 | ), 46 | ("Item 3", "Another example of an error message that is quite lengthy and requires a popup to be fully visible."), 47 | ] 48 | 49 | # Insert data into the Treeview 50 | for item in data: 51 | tree.insert("", "end", text=item[0], values=(item[1],)) 52 | 53 | # Bind double-click event to the Treeview 54 | tree.bind("", on_item_double_click) 55 | 56 | # Start the Tkinter main loop 57 | root.mainloop() 58 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # tests/conftest.py 2 | import os 3 | import shutil 4 | import tempfile 5 | from logging import INFO, WARNING 6 | 7 | # Add the src directory to sys.path dynamically 8 | # sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) 9 | from pathlib import Path 10 | from typing import Any, Generator 11 | 12 | import pytest 13 | 14 | import tests.controller.dicom_pacs_simulator_scp as pacs_simulator_scp 15 | from anonymizer.controller.project import ProjectController 16 | from anonymizer.model.project import LoggingLevels, NetworkTimeouts, ProjectModel 17 | from anonymizer.utils.logging import init_logging 18 | from tests.controller.dicom_test_nodes import ( 19 | TEST_PROJECTNAME, 20 | TEST_SITEID, 21 | TEST_UIDROOT, 22 | LocalSCU, 23 | LocalStorageSCP, 24 | PACSSimulatorSCP, 25 | RemoteSCPDict, 26 | ) 27 | 28 | # def pytest_sessionstart(session): 29 | # """Runs before the test session begins.""" 30 | 31 | 32 | @pytest.fixture 33 | def temp_dir() -> Generator[str, Any, None]: 34 | # Create a temporary directory 35 | temp_path = tempfile.mkdtemp() 36 | 37 | # Initialise logging without file handler: 38 | init_logging(file_handler=False) 39 | 40 | # Yield the directory path to the test function 41 | yield temp_path 42 | 43 | # Remove the temporary directory after the test is done 44 | shutil.rmtree(temp_path) 45 | 46 | 47 | @pytest.fixture 48 | def controller(temp_dir: str) -> Generator[ProjectController, Any, None]: 49 | anon_store = Path(temp_dir, LocalSCU.aet) 50 | # Make sure storage directory exists: 51 | os.makedirs(anon_store, exist_ok=True) 52 | # Create Test ProjectModel: 53 | project_model = ProjectModel( 54 | site_id=TEST_SITEID, 55 | project_name=TEST_PROJECTNAME, 56 | uid_root=TEST_UIDROOT, 57 | remove_pixel_phi=False, 58 | storage_dir=anon_store, 59 | scu=LocalSCU, 60 | scp=LocalStorageSCP, 61 | remote_scps=RemoteSCPDict, 62 | network_timeouts=NetworkTimeouts(2, 5, 5, 15), 63 | anonymizer_script_path=Path( 64 | "src/anonymizer/assets/scripts/default-anonymizer.script" 65 | ), 66 | logging_levels=LoggingLevels( 67 | anonymizer=INFO, pynetdicom=WARNING, pydicom=False 68 | ), 69 | ) 70 | 71 | project_controller = ProjectController(project_model) 72 | 73 | assert project_controller 74 | 75 | project_controller.start_scp() 76 | 77 | # Start PACS Simulator: 78 | assert pacs_simulator_scp.start( 79 | addr=PACSSimulatorSCP, 80 | storage_dir=os.path.join(temp_dir, PACSSimulatorSCP.aet), 81 | known_nodes=[LocalStorageSCP], # one move destination 82 | ) 83 | assert pacs_simulator_scp.server_running() 84 | 85 | yield project_controller 86 | 87 | # Ensure Local Storage is stopped 88 | project_controller.stop_scp() 89 | project_controller.anonymizer.stop() 90 | 91 | # Stop PACS Simulator: 92 | pacs_simulator_scp.stop() 93 | -------------------------------------------------------------------------------- /tests/controller/assets/JavaGeneratedIndex.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/tests/controller/assets/JavaGeneratedIndex.xlsx -------------------------------------------------------------------------------- /tests/controller/assets/test_dcm_files/JPEG-LS_Lossy.dcm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RSNA/anonymizer/e029c287894e8a1f379f8fd46ff67fd416d45c4c/tests/controller/assets/test_dcm_files/JPEG-LS_Lossy.dcm -------------------------------------------------------------------------------- /tests/controller/assets/test_dcm_files/dicomdirtests_README.txt: -------------------------------------------------------------------------------- 1 | In this directory are different variant of a DICOMDIR file representing the 3 patient directories. 2 | 3 | DICOMDIR: 4 | created using dcmmkdir from DCMTK 5 | 6 | DICOMDIR-bigEnd: 7 | created from DICOMDIR using dcmodify by changing the transfer syntax to Explicit Big Endian 8 | 9 | DICOMDIR-implicit: 10 | Created from DICOMDIR using pydicom's `FileSet.write(force_implicit=True)` 11 | 12 | DICOMDIR-nooffset: 13 | created from DICOMDIR by removing some of the 0-offset tags 14 | 15 | DICOMDIR-reordered: 16 | created from DICOMDIR by reordering the first 4 entries (IMAGE - SERIES - STUDY - PATIENT 17 | instead of PATIENT - STUDY - SERIES - IMAGE) and adapting the offsets 18 | 19 | DICOMDIR-nopatient: 20 | created from DICOMDIR by changing the type of the patient records to an invalid type 21 | -------------------------------------------------------------------------------- /tests/controller/dicom_test_files.py: -------------------------------------------------------------------------------- 1 | # TEST FILES from pydicom/data/test_files 2 | # see assets/docs/test files/pydicom_test_files.txt for details 3 | 4 | # Doe^Archibald 5 | CR_STUDY_3_SERIES_3_IMAGES = [ 6 | "./77654033/CR1/6154", 7 | "./77654033/CR2/6247", 8 | "./77654033/CR3/6278", 9 | ] 10 | # Doe^Archibald 11 | CT_STUDY_1_SERIES_4_IMAGES = [ 12 | "./77654033/CT2/17106", 13 | "./77654033/CT2/17136", 14 | "./77654033/CT2/17166", 15 | "./77654033/CT2/17196", 16 | ] 17 | 18 | # Doe^Peter 19 | MR_STUDY_3_SERIES_11_IMAGES = [ 20 | "./98892003/MR1/5641", 21 | "./98892003/MR2/6935", 22 | "./98892003/MR2/6605", 23 | "./98892003/MR2/6273", 24 | "./98892003/MR700/4558", 25 | "./98892003/MR700/4528", 26 | "./98892003/MR700/4588", 27 | "./98892003/MR700/4467", 28 | "./98892003/MR700/4618", 29 | "./98892003/MR700/4678", 30 | "./98892003/MR700/4648", 31 | ] 32 | 33 | # TODO: use @dataclass for TestDCMData 34 | 35 | 36 | patient1_name = "Doe^Archibald" 37 | patient1_id = "77654033" 38 | cr1_filename = CR_STUDY_3_SERIES_3_IMAGES[0] 39 | cr1_SOPInstanceUID = "1.3.6.1.4.1.5962.1.1.0.0.0.1196527414.5534.0.11" 40 | cr1_StudyInstanceUID = "1.3.6.1.4.1.5962.1.1.0.0.0.1196527414.5534.0.1" 41 | cr1_SeriesInstanceUID = "1.3.6.1.4.1.5962.1.1.0.0.0.1196527414.5534.0.10" 42 | 43 | # Brain MRI 3 Series, 11 Images 44 | patient2_name = "Doe^Peter" 45 | patient2_id = "98890234" 46 | mr_brain_StudyInstanceUID = "1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.1" 47 | mr_brain_SeriesInstanceUID = "1.3.6.1.4.1.5962.1.1.0.0.0.1196533885.18148.0.1" 48 | mr_brain_filename = MR_STUDY_3_SERIES_11_IMAGES[0] 49 | 50 | # COMPRESSED Samples 51 | patient3_name = "CompressedSamples^CT1" 52 | patient3_id = "1CT1" 53 | ct_small_filename = "CT_small.dcm" 54 | ct_small_StudyInstanceUID = "1.3.6.1.4.1.5962.1.2.1.20040119072730.12322" 55 | ct_small_SeriesInstanceUID = "1.3.6.1.4.1.5962.1.3.1.1.20040119072730.12322" 56 | ct_small_SOPInstanceUID = "1.3.6.1.4.1.5962.1.1.1.1.1.20040119072730.12322" 57 | 58 | # MR image, Explicit VR, LittleEndian: 59 | patient4_name = "CompressedSamples^MR1" 60 | patient4_id = "4MR1" 61 | mr_small_filename = "MR_small.dcm" 62 | mr_small_StudyInstanceUID = "1.3.6.1.4.1.5962.1.2.4.20040826185059.5457" 63 | mr_small_SeriesInstanceUID = "1.3.6.1.4.1.5962.1.3.4.1.20040826185059.5457" 64 | 65 | # MR_small.dcm image, Implicit VR, LittleEndian 66 | mr_small_implicit_filename = "MR_small_implicit.dcm" 67 | # MR_small.dcm image, Explicit VR, LittleEndian 68 | mr_small_bigendian_filename = "MR_small_bigendian.dcm" 69 | 70 | # Compressed Samples: 71 | # if prefixed by test_files/, then the file is in the test_files directory else part of pydicom test files 72 | COMPRESSED_TEST_FILES = { 73 | "JPEG_Baseline": "SC_jpeg_no_color_transform.dcm", # ".50" JPEG Baseline, Lossy, Non-Hierarchial 74 | "JPEG_Extended": "JPGExtended.dcm", # ".51" JPEG Extended, Lossy, Non-Hierarchial 75 | # "JPEG_Lossless_P14": "", # ".57" JPEG Lossless Nonhierarchical (Processes 14). 76 | "JPEG_Lossless_P14_FOP": "JPEG-LL.dcm", # ".70" JPEG Lossless Nonhierarchical, First-Order Prediction (Processes 14 [Selection Value 1]) 77 | "JPEG-LS_Lossless": "MR_small_jpeg_ls_lossless.dcm", # ".80" JPEG-LS Lossless 78 | "JPEG-LS_Lossy": "test_dcm_files/JPEG-LS_Lossy.dcm", # ".81", JPEG-LS Lossy 79 | "JPEG2000_Lossless": "MR_small_jp2klossless.dcm", # ".90" JPEG 2000 Lossless 80 | "JPEG2000": "JPEG2000.dcm", # ".91" JPEG 2000 81 | } 82 | -------------------------------------------------------------------------------- /tests/controller/dicom_test_nodes.py: -------------------------------------------------------------------------------- 1 | from anonymizer.model.project import DICOMNode 2 | 3 | LocalSCU = DICOMNode("127.0.0.1", 0, "ANONYMIZER", True) 4 | LocalStorageSCP = DICOMNode("127.0.0.1", 1045, "ANONYMIZER", True) 5 | PACSSimulatorSCP = DICOMNode("127.0.0.1", 1046, "TESTPACS", False) 6 | OrthancSCP = DICOMNode("127.0.0.1", 4242, "ORTHANC", False) 7 | 8 | RemoteSCPDict: dict[str, DICOMNode] = { 9 | PACSSimulatorSCP.aet: PACSSimulatorSCP, 10 | OrthancSCP.aet: OrthancSCP, 11 | LocalStorageSCP.aet: LocalStorageSCP, 12 | } 13 | 14 | # Default project globals: 15 | TEST_SITEID = "99.99" 16 | TEST_PROJECTNAME = "ANONYMIZER_UNIT_TEST" 17 | TEST_UIDROOT = "1.2.826.0.1.3680043.10.474" 18 | -------------------------------------------------------------------------------- /tests/controller/test_aws_upload.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from botocore.exceptions import NoCredentialsError 5 | from dotenv import load_dotenv 6 | from pydicom.data import get_testdata_file 7 | 8 | from anonymizer.controller.project import ProjectController 9 | from tests.controller.dicom_test_files import ct_small_filename 10 | 11 | # Load environment variables from .env file (for username/password for AWS upload) 12 | load_dotenv() 13 | 14 | 15 | def test_send_1_dicomfile_to_AWS_S3_and_list_objects(temp_dir: str, controller: ProjectController): 16 | dcm_file_path = str(get_testdata_file(ct_small_filename)) 17 | assert dcm_file_path 18 | assert os.path.exists(dcm_file_path) 19 | 20 | username = os.getenv("AWS_USERNAME") 21 | pw = os.getenv("AWS_PASSWORD") 22 | 23 | assert username 24 | assert pw 25 | 26 | controller.model.aws_cognito.username = username 27 | controller.model.aws_cognito.password = pw 28 | 29 | s3 = controller.AWS_authenticate() 30 | assert s3 31 | 32 | try: 33 | assert controller._aws_user_directory 34 | 35 | object_key: str = Path( 36 | controller.model.aws_cognito.s3_prefix, 37 | controller._aws_user_directory, 38 | controller.model.project_name, 39 | f"{ct_small_filename}", 40 | ).as_posix() 41 | 42 | s3.upload_file(dcm_file_path, controller.model.aws_cognito.s3_bucket, object_key) 43 | 44 | except NoCredentialsError: 45 | raise AssertionError( 46 | "AWS credentials not found. Please set AWS_USERNAME and AWS_PASSWORD in .env file." 47 | ) from NoCredentialsError 48 | 49 | except Exception: 50 | raise AssertionError() from Exception 51 | 52 | # Ensure cached credentials are returned from next call to AWS_authenticate() 53 | s3_b = controller.AWS_authenticate() 54 | assert s3_b == s3 55 | 56 | # List the objects in the bucket at the prefix to ensure the file was uploaded 57 | aws_project_prefix: str = Path( 58 | controller.model.aws_cognito.s3_prefix, controller._aws_user_directory, controller.model.project_name 59 | ).as_posix() 60 | 61 | response = s3.list_objects(Bucket=controller.model.aws_cognito.s3_bucket, Prefix=aws_project_prefix) 62 | 63 | assert "Contents" in response 64 | 65 | aws_files = [obj["Key"] for obj in response["Contents"]] 66 | 67 | assert object_key in aws_files 68 | 69 | # Test ListObjectsV2 paginator 70 | paginator = s3.get_paginator("list_objects_v2") 71 | filenames = [] 72 | 73 | # Initial request with prefix (if provided) 74 | pagination_config = {"Bucket": controller.model.aws_cognito.s3_bucket, "Prefix": aws_project_prefix} 75 | for page in paginator.paginate(**pagination_config): 76 | if "Contents" in page: 77 | filenames.extend([os.path.basename(obj["Key"]) for obj in page["Contents"]]) 78 | 79 | assert ct_small_filename in filenames 80 | -------------------------------------------------------------------------------- /tests/controller/test_dicom_echo_scu.py: -------------------------------------------------------------------------------- 1 | from dicom_test_nodes import LocalStorageSCP, PACSSimulatorSCP 2 | 3 | 4 | def test_echo_pacs_simulator(controller): 5 | assert controller.echo(PACSSimulatorSCP.aet) 6 | 7 | 8 | def test_echo_local_storage(controller): 9 | assert controller.echo(LocalStorageSCP.aet) 10 | -------------------------------------------------------------------------------- /tests/controller/test_load_java_index.py: -------------------------------------------------------------------------------- 1 | from anonymizer.controller.project import ProjectController 2 | from anonymizer.utils.storage import ( 3 | JavaAnonymizerExportedStudy, 4 | read_java_anonymizer_index_xlsx, 5 | ) 6 | 7 | 8 | def test_read_java_anonymizer_index_xlsx(temp_dir: str, controller: ProjectController) -> None: 9 | index_file = "tests/controller/assets/JavaGeneratedIndex.xlsx" 10 | studies: list[JavaAnonymizerExportedStudy] = read_java_anonymizer_index_xlsx(index_file) 11 | assert studies 12 | assert len(studies) == 112 13 | assert studies[0].ANON_PatientName == "527408-000001" 14 | assert studies[0].ANON_PatientID == "527408-000001" 15 | assert studies[0].PHI_PatientName == "TEST" 16 | assert studies[0].PHI_PatientID == "999" 17 | assert studies[69].PHI_PatientName == "Mary Martinez" 18 | assert studies[69].PHI_PatientID == "574856-000200" 19 | 20 | 21 | def test_load_java_index_into_new_project(temp_dir: str, controller: ProjectController) -> None: 22 | index_file = "tests/controller/assets/JavaGeneratedIndex.xlsx" 23 | studies: list[JavaAnonymizerExportedStudy] = read_java_anonymizer_index_xlsx(index_file) 24 | 25 | controller.anonymizer.model.process_java_phi_studies(studies) 26 | assert controller.anonymizer.model.get_patient_id_count() == 83 27 | assert controller.anonymizer.model.get_phi_name("527408-000001") == "TEST" 28 | -------------------------------------------------------------------------------- /tests/controller/test_logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pydicom import config as pydicom_config 4 | 5 | from anonymizer.model.project import LoggingLevels 6 | from src.anonymizer.utils.logging import ( 7 | disable_pydicom_debug, 8 | enable_pydicom_debug, 9 | set_anonymizer_log_level, 10 | set_logging_levels, 11 | set_pynetdicom_log_level, 12 | ) 13 | 14 | 15 | def test_set_logging_levels_all_levels(): 16 | """ 17 | Test setting all logging levels. 18 | """ 19 | levels = LoggingLevels(logging.DEBUG, logging.INFO, True) 20 | 21 | set_logging_levels(levels) 22 | 23 | assert logging.getLogger().getEffectiveLevel() == logging.DEBUG 24 | assert logging.getLogger("pynetdicom").getEffectiveLevel() == logging.INFO 25 | # assert pydicom_config.debug() 26 | 27 | 28 | def test_set_logging_levels_no_pydicom_debug(): 29 | """ 30 | Test setting logging levels without pydicom debug. 31 | """ 32 | levels = LoggingLevels(logging.WARNING, logging.ERROR, False) 33 | 34 | set_logging_levels(levels) 35 | 36 | assert logging.getLogger().getEffectiveLevel() == logging.WARNING 37 | assert logging.getLogger("pynetdicom").getEffectiveLevel() == logging.ERROR 38 | # assert not pydicom_config.debug() 39 | 40 | 41 | def test_set_anonymizer_log_level(): 42 | """ 43 | Test setting the anonymizer log level. 44 | """ 45 | set_anonymizer_log_level(logging.INFO) 46 | assert logging.getLogger().getEffectiveLevel() == logging.INFO 47 | 48 | 49 | def test_set_pynetdicom_log_level(): 50 | """ 51 | Test setting the pynetdicom log level. 52 | """ 53 | set_pynetdicom_log_level(logging.DEBUG) 54 | assert logging.getLogger("pynetdicom").getEffectiveLevel() == logging.DEBUG 55 | 56 | 57 | def test_enable_pydicom_debug(): 58 | """ 59 | Test disabling pydicom debug mode. 60 | """ 61 | enable_pydicom_debug() 62 | assert pydicom_config.debugging 63 | 64 | 65 | def test_disable_pydicom_debug(): 66 | """ 67 | Test disabling pydicom debug mode. 68 | """ 69 | disable_pydicom_debug() 70 | assert not pydicom_config.debugging 71 | -------------------------------------------------------------------------------- /tests/controller/test_modalities.py: -------------------------------------------------------------------------------- 1 | from src.anonymizer.utils.modalities import get_modalities 2 | 3 | 4 | def test_get_modalities(): 5 | """ 6 | Tests if the get_modalities function returns the expected dictionary 7 | structure and contains the expected keys. 8 | """ 9 | data = get_modalities() 10 | 11 | # Assert the data is a dictionary 12 | assert isinstance(data, dict) 13 | 14 | # Assert the dictionary contains the expected keys 15 | expected_keys = ["CR", "DX", "IO", "MG", "CT", "MR", "US", "PT", "NM", "SC", "SR", "PR", "PDF", "OT", "DOC"] 16 | assert set(data.keys()) == set(expected_keys) 17 | -------------------------------------------------------------------------------- /tests/controller/test_network.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from unittest.mock import Mock, patch 3 | 4 | import pytest 5 | 6 | from src.anonymizer.utils.network import dns_lookup, get_local_ip_addresses, is_valid_ip 7 | 8 | 9 | def test_get_local_ip_addresses_localhost() -> None: 10 | """Test that localhost IP is included in the returned addresses""" 11 | ip_addrs = get_local_ip_addresses() 12 | assert ip_addrs 13 | assert "127.0.0.1" in ip_addrs 14 | 15 | 16 | def test_get_local_ip_addresses_excludes_link_local() -> None: 17 | """Test that link-local addresses (169.*) are excluded""" 18 | mock_adapter = Mock() 19 | mock_adapter.ips = [Mock(ip="169.254.1.1"), Mock(ip="192.168.1.1"), Mock(ip="127.0.0.1")] 20 | 21 | with patch("ifaddr.get_adapters", return_value=[mock_adapter]): 22 | ip_addrs = get_local_ip_addresses() 23 | assert "169.254.1.1" not in ip_addrs 24 | assert "192.168.1.1" in ip_addrs 25 | 26 | 27 | def test_get_local_ip_addresses_handles_non_str_ips() -> None: 28 | """Test handling of non-string IP addresses from adapters""" 29 | mock_adapter = Mock() 30 | mock_adapter.ips = [ 31 | Mock(ip=("2001:db8::", 64)), # IPv6 tuple representation 32 | Mock(ip="192.168.1.1"), # Regular IPv4 string 33 | ] 34 | 35 | with patch("ifaddr.get_adapters", return_value=[mock_adapter]): 36 | ip_addrs = get_local_ip_addresses() 37 | assert len(ip_addrs) == 1 38 | assert "192.168.1.1" in ip_addrs 39 | 40 | 41 | def test_dns_lookup_valid_domain() -> None: 42 | """Test DNS lookup with a valid domain""" 43 | with patch("socket.gethostbyname", return_value="93.184.216.34"): 44 | result = dns_lookup("example.com") 45 | assert result == "93.184.216.34" 46 | 47 | 48 | def test_dns_lookup_invalid_domain() -> None: 49 | """Test DNS lookup with an invalid domain""" 50 | with patch("socket.gethostbyname", side_effect=socket.gaierror): 51 | result = dns_lookup("invalid.domain.that.does.not.exist") 52 | assert result == "_DNS Lookup Failed" 53 | 54 | 55 | @pytest.mark.parametrize( 56 | "ip_address,expected", 57 | [ 58 | ("192.168.1.1", True), 59 | ("256.256.256.256", False), 60 | ("2001:0db8:85a3:0000:0000:8a2e:0370:7334", True), 61 | ("not_an_ip", False), 62 | ("192.168.1", False), 63 | ("", False), 64 | ], 65 | ) 66 | def test_is_valid_ip(ip_address: str, expected: bool) -> None: 67 | """Test IP validation with various inputs""" 68 | assert is_valid_ip(ip_address) == expected 69 | 70 | 71 | def test_is_valid_ip_none_input() -> None: 72 | """Test IP validation with None input""" 73 | assert is_valid_ip(None) is False 74 | 75 | 76 | def test_dns_lookup_not_domain() -> None: 77 | """Test DNS lookup with an empty domain string""" 78 | result = dns_lookup("not.a.domain") 79 | assert result == "_DNS Lookup Failed" 80 | -------------------------------------------------------------------------------- /tests/controller/test_translate.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.anonymizer.utils.translate import ( 4 | _current_translations, 5 | get_current_language, 6 | get_current_language_code, 7 | insert_space_after_codes, 8 | insert_spaces_between_cases, 9 | set_language, 10 | set_language_code, 11 | ) 12 | 13 | 14 | def test_set_language_code_valid_code(): 15 | """Test setting language code with a valid code.""" 16 | set_language_code("en_US") 17 | assert get_current_language_code() == "en_US" 18 | 19 | 20 | def test_set_language_code_invalid_code(): 21 | """Test setting language code with an invalid code.""" 22 | with pytest.raises(ValueError) as excinfo: 23 | set_language_code("invalid_code") 24 | assert "Invalid language code" in str(excinfo.value) 25 | 26 | 27 | def test_set_language_valid_language(): 28 | """Test setting language with a valid language.""" 29 | set_language("English") 30 | assert get_current_language() == "English" 31 | 32 | 33 | def test_set_language_invalid_language(): 34 | """Test setting language with an invalid language.""" 35 | with pytest.raises(ValueError) as excinfo: 36 | set_language("invalid_language") 37 | assert "Invalid language" in str(excinfo.value) 38 | 39 | 40 | def test_get_current_language_code_set(): 41 | """Test getting current language code when language is set.""" 42 | set_language_code("fr") 43 | assert get_current_language_code() == "fr" 44 | 45 | 46 | def test_get_current_language_set(): 47 | """Test getting current language when language is set.""" 48 | set_language("Español") 49 | assert get_current_language() == "Español" 50 | 51 | 52 | def test_insert_spaces_between_cases(): 53 | """Test inserting spaces between lowercase and uppercase letters.""" 54 | assert insert_spaces_between_cases("helloWorld") == "hello World" 55 | 56 | 57 | def test_insert_space_after_codes(): 58 | """Test inserting spaces after codes in a string.""" 59 | codes = ["HTTP", "URL"] 60 | assert insert_space_after_codes("This is a HTTP URL", codes) == "This is a HTTP URL " 61 | 62 | 63 | def test_translation_after_set_language_code(): 64 | """Test if _current_translations is set after set_language_code.""" 65 | set_language_code("de") 66 | assert _current_translations is not None 67 | --------------------------------------------------------------------------------