Wählen Sie im Menü Datei/Neues Projekt aus und es erscheint ein Dialogfeld für Neue Projekteinstellungen.
14 |
Weitere Informationen finden Sie unter Hilfe/Projekteinstellungen.
15 |
16 |
17 |
Schließen eines Projekts
18 |
19 |
20 |
Wählen Sie im Menü Datei/Projekt schließen oder schließen Sie einfach das Fenster mit der Schaltfläche X.
21 |
22 |
23 |
Wiedereröffnen eines Projekts
24 |
25 |
26 |
Wählen Sie im Menü Datei/Zuletzt geöffnete, um eine Liste der zuvor geöffneten Projekte anzuzeigen.
27 |
Klicken Sie auf den Projektnamen, um es wieder zu öffnen.
28 |
Beim Start wird der Anonymisierer das letzte Projekt wieder öffnen, wenn es beim Herunterfahren des Anonymisierers nicht geschlossen wurde.
29 |
30 |
31 |
Klonen eines Projekts
32 |
33 |
34 |
Wählen Sie im Menü Datei/Klonen, um die Einstellungen des aktuellen Projekts in ein sauberes neues Projekt zu klonen.
35 |
Wählen Sie das neue Speicherverzeichnis für den Klon aus.
36 |
Ein Dialogfeld Neue Projekteinstellungen wird angezeigt, das das Bearbeiten von Einstellungen ermöglicht, einschließlich des neuen Projektnamens und der UID-Wurzel.
37 |
Nur die aktuellen Projekteinstellungen über die Datei Project.pkl werden kopiert, keine Bilddateien.
38 |
Die Standort-ID wird beibehalten.
39 |
Warnung: Stellen Sie sicher, dass die UID-Wurzel für alle Ihre Projekte eindeutig ist, um UID-Konflikte zu vermeiden.
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 |
13 |
Desde el menú seleccione Archivo/Nuevo Proyecto y aparecerá el cuadro de diálogo de Configuración de Nuevo Proyecto.
14 |
Consulte Ayuda/Configuración del Proyecto para obtener detalles completos.
15 |
16 |
17 |
Cierre de un Proyecto
18 |
19 |
20 |
Desde el menú, seleccione Archivo/Cerrar Proyecto o simplemente cierre la ventana con el botón X.
21 |
22 |
23 |
Reapertura de un Proyecto
24 |
25 |
26 |
Desde el menú, seleccione Archivo/Abrir Reciente para ver una lista de proyectos abiertos anteriormente.
27 |
Haga clic en el nombre del proyecto para reabrirlo.
28 |
Al iniciar, el Anonimizador reabrirá el último proyecto si no se cerró cuando se apagó el Anonimizador.
29 |
30 |
31 |
Clonación de un Proyecto
32 |
33 |
34 |
Desde el menú, seleccione Archivo/Clonar para clonar la configuración del proyecto actual en un nuevo proyecto limpio.
35 |
Seleccione el nuevo directorio de almacenamiento para el clon.
36 |
Luego se presenta un cuadro de diálogo de Configuración de Nuevo Proyecto que permite editar cualquier configuración, incluido el nuevo nombre del proyecto y la Raíz UID.
37 |
Sólo se copiarán los ajustes del proyecto actual a través del archivo Project.pkl, no se copiarán archivos de imágenes.
38 |
Se preservará el ID del sitio.
39 |
Advertencia: asegúrese de que la Raíz UID sea única para todos sus proyectos para evitar conflictos de UID.
22 | El software mencionado anteriormente, incluido en el RSNA DICOM Anonymizer, se publica bajo las siguientes licencias:
23 |
24 |
HPND: Pillow
25 |
Apache 2.0: boto3
26 |
OSI: pywin32-ctypes
27 |
MIT: todos los demás
28 |
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 |
13 |
Dans le menu, sélectionnez Fichier/Nouveau Projet et la boîte de dialogue Paramètres du Nouveau Projet apparaîtra.
14 |
Référez-vous à Aide/Paramètres du Projet pour plus de détails.
15 |
16 |
17 |
Fermeture d'un Projet
18 |
19 |
20 |
Dans le menu, sélectionnez Fichier/Fermer le Projet ou fermez simplement la fenêtre avec le bouton X.
21 |
22 |
23 |
Réouverture d'un Projet
24 |
25 |
26 |
Dans le menu, sélectionnez Fichier/Ouvrir Récent pour voir une liste des projets ouverts précédemment.
27 |
Cliquez sur le nom du projet pour le rouvrir.
28 |
Au démarrage, l'Anonymiseur rouvrira le dernier projet s'il n'a pas été fermé lors de l'arrêt de l'Anonymiseur.
29 |
30 |
31 |
Clonage d'un Projet
32 |
33 |
34 |
Dans le menu, sélectionnez Fichier/Cloner pour cloner les paramètres du projet actuel dans un nouveau projet propre.
35 |
Sélectionnez le nouveau répertoire de stockage pour le clone.
36 |
Une boîte de dialogue Paramètres du Nouveau Projet est alors présentée, permettant de modifier tous les paramètres, y compris le nouveau nom du projet et la Racine UID.
37 |
Seuls les paramètres du projet actuel via le fichier Project.pkl seront copiés, aucun fichier image ne sera copié.
38 |
L'ID du site sera préservé.
39 |
Attention : assurez-vous que la Racine UID est unique pour tous vos projets afin d'éviter les conflits de UID.
22 | Les logiciels ci-dessus, inclus dans le RSNA DICOM Anonymizer, sont publiés sous les licences suivantes :
23 |
24 |
HPND: Pillow
25 |
Apache 2.0: boto3
26 |
OSI: pywin32-ctypes
27 |
MIT: tous les autres
28 |
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 |
--------------------------------------------------------------------------------