diff --git a/README.md b/README.md index 037ec7b..a2a22f8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,144 @@ # pydicom-migrasi-clarity -Untuk migrasi PACS Clarity ke ABPACS dengan cara: FindSCU by date, GetSCU for each StudyIUID found, StoreSCU to ABPACS, then send API to HIS2 to fill pacs_result_series \ No newline at end of file +## Tujuan Proyek +Aplikasi ini dibuat untuk melakukan migrasi data PACS dari sistem Clarity ke ABPACS dengan proses sebagai berikut: +1. Melakukan pencarian studi DICOM menggunakan FindSCU berdasarkan tanggal +2. Mengambil data DICOM lengkap menggunakan GetSCU untuk setiap StudyIUID yang ditemukan +3. Mengirim data DICOM ke ABPACS menggunakan StoreSCU +4. Mengirim data ke API HIS2 untuk mengisi pacs_result_series +5. Mencatat seluruh proses dalam log terstruktur + +## Alur Kerja Detail +Aplikasi ini menggunakan pendekatan berbasis Python murni dengan bantuan library pynetdicom. Berikut tahapan detail proses: + +1. **Pencarian (FindSCU)**: + - Mencari studi berdasarkan rentang tanggal (StudyDate) + - Mengekstrak metadata penting seperti StudyInstanceUID, AccessionNumber, PatientID + - Menyimpan hasil pencarian dalam format JSON + +2. **Pengambilan (GetSCU)**: + - Mengunduh seluruh data DICOM untuk setiap StudyInstanceUID yang ditemukan + - Menyimpan data DICOM dalam struktur direktori terorganisir + - Data disimpan sementara dan dihapus otomatis setelah pengiriman + +3. **Pengiriman (StoreSCU)**: + - Mengirim data DICOM yang diunduh ke server ABPACS + - Melacak status pengiriman dan menangani error atau kegagalan + - Menghapus data DICOM setelah pengiriman berhasil + +4. **Integrasi HIS**: + - Mengirim metadata DICOM ke API HIS2 + - Memastikan informasi studi dan series tersimpan di sistem HIS + - Menyimpan log hasil komunikasi dengan sistem HIS + +## Persyaratan +- Python 3.9 +- Pustaka Python utama: + - pydicom + - pynetdicom + - requests + - python-dateutil + - retry +- Akses jaringan ke server PACS sumber dan tujuan + +## Cara Mengkloning Repositori + +```bash +git clone https://devone.aplikasi.web.id/gitea/mario/pydicom-migrasi-clarity.git +cd pydicom-migrasi-clarity +``` + +## Instalasi dengan Virtual Environment + +### 1. Membuat Virtual Environment +```bash +# Untuk Linux +python3.9 -m venv venv + +# Untuk Windows +python -m venv venv +``` + +### 2. Mengaktifkan Virtual Environment +```bash +# Untuk Linux +source venv/bin/activate + +# Untuk windows +venv/Scripts/activate +``` + +### 3. Menginstall Dependensi +```bash +pip install -r requirements.txt +``` + +## Konfigurasi PACS + +**Sesuaikan `SOURCE_PACS` dan `DESTINATION_PACS` pada `config/settings.py`** + +## Cara Menjalankan Aplikasi + +### Aktifkan venv +```bash +source /home/pacs/pydicom-migrasi-clarity/venv/bin/activate +``` + +### Contoh Penggunaan Umum +Untuk migrasi data harian baru: +```bash +python main.py process --start-date 20250507 --end-date 20250507 +``` + +### Perintah pendukung yang Tersedia + +Program ini mendukung beberapa mode operasi: + +1. **Process** - Menjalankan alur kerja lengkap (pencarian, pengambilan, pengiriman): + ```bash + python main.py process --start-date 20250501 --end-date 20250502 + ``` + +2. **Process-Study** - Menjalankan alur kerja lengkap untuk studi tertentu: + ```bash + python main.py process-study --study-uid 1.2.826.1.3680043.9.5282.150415.30338.202504010001 + ``` +3. **Find-Studies** - Hanya mencari studi berdasarkan rentang tanggal: + ```bash + python main.py find-studies --start-date 20250501 --end-date 20250502 + ``` + +4. **Get-Study** - Mengambil studi tertentu berdasarkan StudyInstanceUID: + ```bash + python main.py get-study --study-uid 1.2.826.1.3680043.9.5282.150415.30338.202504010001 + ``` + +5. **Send-Study** - Mengirim studi tertentu ke PACS tujuan: + ```bash + python main.py send-study --study-uid 1.2.826.1.3680043.9.5282.150415.30338.202504010001 + ``` + +### Parameter Umum +* `--start-date`: Tanggal awal pencarian dalam format YYYYMMDD +* `--end-date`: Tanggal akhir pencarian dalam format YYYYMMDD +* `--study-uid`: StudyInstanceUID untuk operasi pada satu studi +* `--series-uid`: SeriesInstanceUID untuk operasi pada satu seri +* `--skip-existing`: Lewati studi yang sudah memiliki log (untuk melanjutkan proses yang terhenti) + + + +## Struktur Direktori +- `config/`: Berisi file konfigurasi aplikasi +- `logs/`: Menyimpan log operasi dan hasil pengiriman +- `output/`: Tempat menyimpan hasil DICOM dan file JSON +- `services/`: Modul-modul untuk operasi DICOM (finder, retriever, sender) +- `utils/`: Fungsi-fungsi pembantu dan utilitas + +## Troubleshooting + +Jika menemui masalah, silakan periksa file log di direktori `logs/` untuk informasi lebih detail. + +Untuk menonaktifkan virtual environment setelah selesai: +```bash +deactivate +``` \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index a8ca53f..a877af7 100644 --- a/config/settings.py +++ b/config/settings.py @@ -8,14 +8,14 @@ SOURCE_PORT = 8888 # Our port # Source PACS Configuration (where to query/get data from) SOURCE_PACS = { - "host": "192.168.22.3", + "host": "192.168.2.30", "port": 11112, - "aet": "ABPACS" + "aet": "FREEDOM" } # Destination PACS Configuration (where to send data to) DESTINATION_PACS = { - "host": "152.42.173.210", + "host": "192.168.1.29", "port": 11112, "aet": "ABPACS" } @@ -40,5 +40,5 @@ JSON_OUTPUT_DIR = "output/json" DICOM_STORE_DIR = "output/dicom" # HIS Configuration -HIS_HOST = "localhost:8787" -HIS_URL = "/result_series" \ No newline at end of file +HIS_HOST = "192.168.1.1:80" +HIS_URL = "/poapi/api_migrasiclarity.php?name=migrasi_data_pacs" diff --git a/main.py b/main.py index b4352f7..32e5f15 100644 --- a/main.py +++ b/main.py @@ -15,6 +15,7 @@ from utils.cleanup import register_exit_handlers, register_cleanup_dir from services.dicom_finder import DicomFinder from services.dicom_retriever import DicomRetriever from services.dicom_sender import DicomSender +import signal def parse_args(): """Parse command line arguments.""" @@ -63,6 +64,11 @@ def parse_args(): send_file_parser = subparsers.add_parser('send-file', help='Send a single DICOM file to destination PACS') send_file_parser.add_argument('--file-path', required=True, help='Path to DICOM file to send') + # Add to the parse_args() function after the 'process' command + process_study_parser = subparsers.add_parser('process-study', help='Process full workflow for a specific study by StudyInstanceUID') + process_study_parser.add_argument('--study-uid', required=True, help='StudyInstanceUID to process') + process_study_parser.add_argument('--log-dir', help='Directory to save JSON logs (default: JSON_OUTPUT_DIR)') + # Process command - full workflow: find, get, and send DICOM data for a date range process_parser = subparsers.add_parser('process', help='Process full workflow (find, get, send) for a date range') process_parser.add_argument('--start-date', required=True, help='Start date in YYYYMMDD format') @@ -357,6 +363,150 @@ def send_file(args): logger.error(f"Error sending file: {str(e)}") sys.exit(1) +def process_single_study(study_uid, accession_number=None, log_dir=None): + """ + Process a single study: retrieve it, send to destination PACS, + and create detailed JSON logs. + + Args: + study_uid: Study Instance UID + accession_number: Optional accession number + log_dir: Directory to save logs + + Returns: + dict: Result information + """ + log_dir = log_dir or settings.JSON_OUTPUT_DIR + + # Create services + finder = DicomFinder() + retriever = DicomRetriever() + sender = DicomSender() + + # First, get the study metadata with a query if not provided + if not accession_number: + study_metadata = finder.find_study_by_uid(study_uid) + if not study_metadata: + logger.error(f"Study not found: {study_uid}") + return {'success': False, 'error': 'Study not found'} + accession_number = getattr(study_metadata, 'AccessionNumber', '') + else: + study_metadata = None + + logger.info(f"Processing study: {accession_number} with Study_IUID ({study_uid})") + + # STEP 1: Retrieve the study + retrieve_result = retriever.retrieve_study(study_uid, accession_number=accession_number) + + if not retrieve_result['success']: + logger.error(f"Failed to retrieve study {study_uid}: {retrieve_result['status']}") + return {'success': False, 'error': f"Failed to retrieve study: {retrieve_result['status']}"} + + logger.info(f"Retrieved {retrieve_result['successful_instances']} instances for study {study_uid}") + + # STEP 2: Send the study to destination PACS + send_result = sender.send_study(os.path.join(settings.DICOM_STORE_DIR, study_uid)) + + if not send_result['success']: + logger.error(f"Failed to send study {study_uid}: {send_result.get('error', 'Unknown error')}") + + logger.info(f"Sent {send_result['successful_sends']} of {send_result['total_files']} files to destination PACS") + + # STEP 3: Find all series and create detailed logs + series_list = finder.find_series_for_study(study_uid, accession_number=accession_number) + + # Get metadata from study_metadata if available, otherwise from the first element of study list + if study_metadata: + study_date = getattr(study_metadata, 'StudyDate', '') + study_time = getattr(study_metadata, 'StudyTime', '000000') + patient_id = getattr(study_metadata, 'PatientID', '') + study_description = getattr(study_metadata, 'StudyDescription', '') + else: + # We need to query for it again + study_details = finder.find_study_by_uid(study_uid) + study_date = getattr(study_details, 'StudyDate', '') if study_details else '' + study_time = getattr(study_details, 'StudyTime', '000000') if study_details else '000000' + patient_id = getattr(study_details, 'PatientID', '') if study_details else '' + study_description = getattr(study_details, 'StudyDescription', '') if study_details else '' + + study_datetime = f"{study_date}{study_time}" + + study_log = { + 'Study_IUID': study_uid, + 'AccessionNumber': accession_number, + 'PatientID': patient_id, + 'StudyDescription': study_description, + 'StudyDateTime': study_datetime, + 'CstoreSuccess': send_result['success'], + 'Series': [] + } + + for series in series_list: + series_uid = series.SeriesInstanceUID + + # Get first instance for the series + instance = finder.find_first_instance_for_series(study_uid, series_uid) + + # Kalau null atau None, forced ke '' + def safe_get_attr(obj, attr_name, default=''): + """Get attribute value, ensuring None is converted to default.""" + value = getattr(obj, attr_name, default) + return default if value is None else str(value) + + series_info = { + 'Series_IUID': safe_get_attr(series, 'SeriesInstanceUID'), + 'SeriesNumber': safe_get_attr(series, 'SeriesNumber'), + 'SeriesDescription': safe_get_attr(series, 'SeriesDescription'), + 'NumberOfInstances': safe_get_attr(series, 'NumberOfSeriesRelatedInstances'), + 'SOP_IUID': safe_get_attr(instance, 'SOPInstanceUID') if instance else '' + } + + study_log['Series'].append(series_info) + + # STEP 4: Send study_log to HIS API + his_url = f"http://{settings.HIS_HOST}{settings.HIS_URL}" + try: + headers = { + 'id': 'Vmtaa2MySnRUblJTYWtKb1ZucHNNVlZVU2pSaFIwNTBZa1JDYkZaV1NtOWFSV1JIVmxkSmQxSnJUbFpTVlZwRlZsaGpPVkJSUFQwPQ==', + 'Content-Type': 'application/json' + } + response = requests.post(his_url, json=study_log, headers=headers) + + # Add response data to the log + study_log['HisApiResponse'] = { + 'StatusCode': response.status_code, + 'ResponseText': response.text, + 'Success': False, + 'Timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + # Safer JSON parsing + try: + response_data = response.json() if response.content else {} + study_log['HisApiResponse']['ResponseData'] = response_data + + if response.status_code == 200 and response_data.get('OK') == "1": + study_log['HisApiResponse']['Success'] = True + logger.info(f"Successfully sent JSON {accession_number} to HIS API") + return {'success': True, 'study_log': study_log, 'his_success': True} + else: + error_msg = response_data.get('message', 'Unknown error') + logger.error(f"Failed to send JSON for {accession_number} to HIS API: {error_msg}. Study_IUID: {study_uid}") + return {'success': True, 'study_log': study_log, 'his_success': False} + except ValueError: + study_log['HisApiResponse']['ParseError'] = "Invalid JSON response" + logger.error(f"Failed to parse response for {accession_number}, invalid JSON response. Status code: {response.status_code}") + return {'success': True, 'study_log': study_log, 'his_success': False} + + except Exception as e: + study_log['HisApiResponse'] = { + 'Success': False, + 'Error': str(e), + 'Timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + logger.error(f"Error sending study for {accession_number} to HIS API: {str(e)}") + return {'success': True, 'study_log': study_log, 'his_success': False, 'error': str(e)} + def process_workflow(args): """ Process the full workflow: find studies by date range, retrieve them, and send to destination PACS. @@ -387,6 +537,7 @@ def process_workflow(args): # Create directory for logs create_directory_if_not_exists(log_dir) + create_directory_if_not_exists("logs") # STEP 1: Find studies in the date range try: @@ -406,12 +557,30 @@ def process_workflow(args): save_json_data(studies_summary, f"studies_{start_date}_to_{end_date}.json", log_dir) # STEP 2, 3, 4: For each study, retrieve, send, and log details - his_log_filename = f"sendtohis_{start_date}_{end_date}.json" - his_log_path = os.path.join("logs", his_log_filename) his_log = [] - his_fail_log_filename = f"fail_sendtohis_{start_date}_{end_date}.json" - his_fail_log_path = os.path.join("logs", his_fail_log_filename) his_fail_log = [] + his_log_filename = f"sendtohis_{start_date}_{end_date}.json" + his_fail_log_filename = f"fail_sendtohis_{start_date}_{end_date}.json" + + # Register signal handler for SIGINT + original_sigint_handler = signal.getsignal(signal.SIGINT) + + def sigint_handler(sig, frame): + logger.warning("Process interrupted by user. Saving logs before exiting...") + # Save HIS logs + if his_log: + save_json_data(his_log, his_log_filename, "logs") + logger.info(f"Saved {len(his_log)} successful logs to {his_log_filename}") + if his_fail_log: + save_json_data(his_fail_log, his_fail_log_filename, "logs") + logger.info(f"Saved {len(his_fail_log)} failed logs to {his_fail_log_filename}") + # Restore original signal handler and raise KeyboardInterrupt + signal.signal(signal.SIGINT, original_sigint_handler) + sys.exit(1) + + # Set up our custom signal handler + signal.signal(signal.SIGINT, sigint_handler) + for i, study in enumerate(studies): accession_number = study.AccessionNumber @@ -424,77 +593,35 @@ def process_workflow(args): logger.info(f"Skipping study {study_uid} - log file already exists") continue - # STEP 2: Retrieve the study try: - retrieve_result = retriever.retrieve_study(study_uid, accession_number=accession_number) - if not retrieve_result['success']: - logger.error(f"Failed to retrieve study {study_uid}: {retrieve_result['status']}") + result = process_single_study(study_uid, accession_number, log_dir) + + if not result['success']: + logger.error(f"Failed to process study {study_uid}: {result.get('error', 'Unknown error')}") continue - logger.info(f"Retrieved {retrieve_result['successful_instances']} instances for study {study_uid}") + # Record results for HIS logs + if result.get('his_success', False): + his_log.append(result['study_log']) + else: + his_fail_log.append(result['study_log']) - # STEP 3: Send the study to destination PACS - send_result = sender.send_study(os.path.join(settings.DICOM_STORE_DIR, study_uid)) - if not send_result['success']: - logger.error(f"Failed to send study {study_uid}: {send_result.get('error', 'Unknown error')}") - - logger.info(f"Sent {send_result['successful_sends']} of {send_result['total_files']} files to destination PACS") - - # STEP 4: Find all series and create detailed logs - series_list = finder.find_series_for_study(study_uid, accession_number=accession_number) - - study_date = getattr(study, 'StudyDate', '') - study_time = getattr(study, 'StudyTime', '000000') # Default to midnight if StudyTime is not available - study_datetime = f"{study_date}{study_time}" - - study_log = { - 'Study_IUID': study_uid, - 'AccessionNumber': getattr(study, 'AccessionNumber', ''), - 'PatientID': getattr(study, 'PatientID', ''), # MedrecID - 'StudyDescription': getattr(study, 'StudyDescription', ''), - 'StudyDateTime': study_datetime, - 'CstoreSuccess': send_result['success'], - 'Series': [] - } - - for series in series_list: - series_uid = series.SeriesInstanceUID + # Save progress incrementally every 5 studies + if (i + 1) % 5 == 0: + if his_log: + save_json_data(his_log, his_log_filename, "logs") + if his_fail_log: + save_json_data(his_fail_log, his_fail_log_filename, "logs") + logger.info(f"Saved progress after processing {i+1} studies") - # Get first instance for the series - instance = finder.find_first_instance_for_series(study_uid, series_uid) - - series_info = { - 'Series_IUID': getattr(series, 'SeriesInstanceUID', ''), - 'SeriesNumber': getattr(series, 'SeriesNumber', ''), - 'SeriesDescription': getattr(series, 'SeriesDescription', ''), - 'NumberOfInstances': getattr(series, 'NumberOfSeriesRelatedInstances', ''), - 'SOP_IUID': getattr(instance, 'SOPInstanceUID', '') if instance else '' - } - - study_log['Series'].append(series_info) - - # Save the detailed log - # save_json_data(study_log, f"{study_uid}.json", log_dir) - # logger.info(f"Created detailed log for study {study_uid}") - - # STEP 5: Send study_log to HIS API - his_url = f"http://{settings.HIS_HOST}{settings.HIS_URL}" - try: - response = requests.post(his_url, json=study_log) - if response.status_code == 200 and response.json().get('status') == "OK": - his_log.append(study_log) - logger.info(f"Successfully sent JSON {accession_number} to HIS API") - else: - his_fail_log.append(study_log) - logger.error(f"Failed to send JSON for {accession_number} to HIS API: {response.msg}. Study_IUID: {study_uid}") - except Exception as e: - logger.error(f"Error sending study for {accession_number} to HIS API: {str(e)}") - except Exception as e: logger.error(f"Error processing study {accession_number}: {str(e)}") continue - # Save HIS log + # Restore original signal handler + signal.signal(signal.SIGINT, original_sigint_handler) + + # Final Save HIS logs if his_log: save_json_data(his_log, his_log_filename, "logs") if his_fail_log: @@ -506,6 +633,77 @@ def process_workflow(args): logger.error(f"Error during workflow processing: {str(e)}") sys.exit(1) +def process_workflow_by_study(args): + """ + Process the full workflow for a specific study. + + Args: + args: Command line arguments + """ + study_uid = args.study_uid + log_dir = args.log_dir or settings.JSON_OUTPUT_DIR + + logger.info(f"Starting full workflow process for study: {study_uid}") + + # Create directory for logs + create_directory_if_not_exists(log_dir) + create_directory_if_not_exists("logs") + + # Register signal handler for SIGINT + original_sigint_handler = signal.getsignal(signal.SIGINT) + + # Create empty containers for logs + his_log = [] + his_fail_log = [] + his_log_filename = f"sendtohis_study_{study_uid}.json" + his_fail_log_filename = f"fail_sendtohis_study_{study_uid}.json" + + def sigint_handler(sig, frame): + logger.warning("Process interrupted by user. Saving logs before exiting...") + # Save individual study log + if 'result' in locals() and result.get('success', False): + save_json_data(result['study_log'], f"{study_uid}.json", log_dir) + + # Save HIS logs + if result.get('his_success', False): + save_json_data([result['study_log']], his_log_filename, "logs") + logger.info(f"Saved successful log to {his_log_filename}") + else: + save_json_data([result['study_log']], his_fail_log_filename, "logs") + logger.info(f"Saved failed log to {his_fail_log_filename}") + + # Restore original signal handler and exit + signal.signal(signal.SIGINT, original_sigint_handler) + sys.exit(1) + + # Set up our custom signal handler + signal.signal(signal.SIGINT, sigint_handler) + + try: + result = process_single_study(study_uid, log_dir=log_dir) + + if not result['success']: + logger.error(f"Failed to process study {study_uid}: {result.get('error', 'Unknown error')}") + sys.exit(1) + + # Save individual study log + save_json_data(result['study_log'], f"{study_uid}.json", log_dir) + + # Save logs + if result.get('his_success', False): + save_json_data([result['study_log']], his_log_filename, "logs") + else: + save_json_data([result['study_log']], his_fail_log_filename, "logs") + + logger.info(f"Completed processing study: {study_uid}") + + except Exception as e: + logger.error(f"Error during study processing: {str(e)}") + sys.exit(1) + finally: + # Restore original signal handler + signal.signal(signal.SIGINT, original_sigint_handler) + def main(): """Main function.""" # Register cleanup handlers for graceful exit @@ -544,6 +742,8 @@ def main(): send_file(args) elif args.command == 'process': process_workflow(args) + elif args.command == 'process-study': + process_workflow_by_study(args) else: logger.error("No command specified. Use --help for options.") sys.exit(1) diff --git a/services/dicom_finder.py b/services/dicom_finder.py index f046f74..1de17c7 100644 --- a/services/dicom_finder.py +++ b/services/dicom_finder.py @@ -269,4 +269,82 @@ class DicomFinder: logger.error(error_msg) raise DicomQueryError(error_msg) - return first_instance \ No newline at end of file + return first_instance + + @dicom_retry(exception_types=(DicomQueryError, ConnectionError)) + def find_study_by_uid(self, study_instance_uid): + """ + Find a specific study by its StudyInstanceUID. + + Args: + study_instance_uid (str): StudyInstanceUID to find + + Returns: + Dataset: Study dataset or None if not found + """ + logger.info(f"Finding study with UID: {study_instance_uid}") + + # Create query dataset + ds = Dataset() + ds.QueryRetrieveLevel = 'STUDY' + + # Set the StudyInstanceUID filter + ds.StudyInstanceUID = study_instance_uid + + # Required fields (minimal set) + ds.StudyDate = '' + + # Additional fields we want to retrieve + ds.AccessionNumber = '' + ds.PatientID = '' # MedrecID + ds.StudyDescription = '' + ds.StudyTime = '' + ds.NumberOfStudyRelatedSeries = '' + + # Create association + assoc = self.ae.associate( + self.pacs_config['host'], + self.pacs_config['port'], + ae_title=self.pacs_config['aet'] + ) + + study = None + + if assoc.is_established: + try: + logger.debug("Association established, sending C-FIND request") + + # Send C-FIND request + responses = assoc.send_c_find( + ds, + StudyRootQueryRetrieveInformationModelFind + ) + + # Process responses - just get the first one + for (status, dataset) in responses: + if status and status.Status == 0xFF00: # Pending + if dataset: + study = dataset + logger.debug(f"Found study: {dataset.StudyInstanceUID}") + break + elif status and status.Status != 0x0000: # Not success + logger.error(f"C-FIND error: {status}") + + if study: + logger.info(f"Found study {study_instance_uid}") + else: + logger.warning(f"No study found with UID {study_instance_uid}") + + except Exception as e: + logger.error(f"Error during C-FIND: {str(e)}") + raise DicomQueryError(f"C-FIND operation failed: {str(e)}") + finally: + # Release the association + assoc.release() + logger.debug("Association released") + else: + error_msg = f"Association rejected, aborted or never connected to {self.pacs_config['aet']}" + logger.error(error_msg) + raise DicomQueryError(error_msg) + + return study diff --git a/services/dicom_retriever.py b/services/dicom_retriever.py index 201ac02..90fa839 100644 --- a/services/dicom_retriever.py +++ b/services/dicom_retriever.py @@ -39,6 +39,7 @@ class DicomRetriever: # Add storage presentation contexts (needed for receiving the images) # for context in StoragePresentationContexts: # self.ae.add_requested_context(context.abstract_syntax) + storage_uids = [ '1.2.840.10008.5.1.4.1.1.1', # CR Storage '1.2.840.10008.5.1.4.1.1.1.1', # Digital X-Ray Image Storage diff --git a/services/dicom_sender.py b/services/dicom_sender.py index f152c73..eb99417 100644 --- a/services/dicom_sender.py +++ b/services/dicom_sender.py @@ -6,11 +6,15 @@ import glob import pydicom import shutil from pydicom.dataset import Dataset -from pynetdicom import AE, StoragePresentationContexts, evt +from pynetdicom import AE, StoragePresentationContexts, evt, build_role from config import settings from utils.logger import main_logger as logger from utils.error_handler import DicomStoreError, dicom_retry from utils.dicom_utils import create_directory_if_not_exists +from pynetdicom.sop_class import ( + StudyRootQueryRetrieveInformationModelGet, + PatientRootQueryRetrieveInformationModelGet +) class DicomSender: """ @@ -27,9 +31,32 @@ class DicomSender: self.pacs_config = pacs_config or settings.DESTINATION_PACS self.ae = AE(ae_title=settings.SOURCE_AET) - # Add storage presentation contexts (all standard transfer syntaxes for each SOP class) - for context in StoragePresentationContexts: - self.ae.add_requested_context(context.abstract_syntax) + # Add the Query/Retrieve SOP classes + self.ae.add_requested_context(StudyRootQueryRetrieveInformationModelGet) + self.ae.add_requested_context(PatientRootQueryRetrieveInformationModelGet) + + # Add storage presentation contexts (needed for receiving the images) + # for context in StoragePresentationContexts: + # self.ae.add_requested_context(context.abstract_syntax) + + storage_uids = [ + '1.2.840.10008.5.1.4.1.1.1', # CR Storage + '1.2.840.10008.5.1.4.1.1.1.1', # Digital X-Ray Image Storage + '1.2.840.10008.5.1.4.1.1.2', # CT Image Storage + '1.2.840.10008.5.1.4.1.1.4', # MR Image Storage + '1.2.840.10008.5.1.4.1.1.7', # Secondary Capture Image Storage + '1.2.840.10008.5.1.4.1.1.6.1', # Ultrasound Image Storage + '1.2.840.10008.5.1.4.1.1.128', # PET Image Storage + '1.2.840.10008.5.1.4.1.1.20', # Nuclear Medicine Image Storage + '1.2.840.10008.5.1.4.1.1.9.1.1', # 12-lead ECG Waveform Storage + '1.2.840.10008.5.1.4.1.1.9.1.2', # General ECG Waveform Storage + ] + self.ext_neg = [] + for uid in storage_uids: + self.ae.add_requested_context(uid) + role = build_role(uid, scp_role=True) + self.ext_neg.append(role) + logger.info(f"DicomSender initialized with destination PACS: {self.pacs_config['aet']}@{self.pacs_config['host']}:{self.pacs_config['port']}")