-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathqr.py
More file actions
executable file
·221 lines (199 loc) · 8.31 KB
/
qr.py
File metadata and controls
executable file
·221 lines (199 loc) · 8.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# qr.py - DICOM Query/Retrieve utility for XRayVision
#
# This script queries a remote DICOM PACS for CR (Computed Radiography) studies
# for a specified date range and requests them to be sent to the local AE.
# It's designed to work with the XRayVision system configuration.
import argparse
import logging
import time
from datetime import datetime, timedelta
import configparser
import os
import sqlite3
from pynetdicom import AE, QueryRetrievePresentationContexts
from pynetdicom.sop_class import PatientRootQueryRetrieveInformationModelFind, PatientRootQueryRetrieveInformationModelMove
from pydicom.dataset import Dataset
# Logger config
logging.basicConfig(
level = logging.INFO,
format = '%(asctime)s | %(levelname)8s | %(message)s',
handlers = [
#logging.FileHandler("xrayvision.log"),
logging.StreamHandler()
]
)
# DICOM network operations
logging.getLogger('pynetdicom').setLevel(logging.WARNING)
# DICOM file operations
logging.getLogger('pydicom').setLevel(logging.WARNING)
# Default configuration values
DEFAULT_CONFIG = {
'dicom': {
'AE_TITLE': 'XRAYVISION',
'AE_PORT': '4010',
'REMOTE_AE_TITLE': 'DICOM_SERVER',
'REMOTE_AE_IP': '192.168.1.1',
'REMOTE_AE_PORT': '104'
}
}
# Load configuration from file if it exists, otherwise use defaults
config = configparser.ConfigParser()
config.read_dict(DEFAULT_CONFIG)
try:
config.read('xrayvision.cfg')
logging.debug("Configuration loaded from xrayvision.cfg")
# Check for local configuration file to override settings
local_config_files = config.read('local.cfg')
if local_config_files:
logging.debug("Local configuration loaded from local.cfg")
except Exception as e:
logging.debug("Using default configuration values")
# Extract configuration values
AE_TITLE = config.get('dicom', 'AE_TITLE')
AE_PORT = config.getint('dicom', 'AE_PORT')
REMOTE_AE_TITLE = config.get('dicom', 'REMOTE_AE_TITLE')
REMOTE_AE_IP = config.get('dicom', 'REMOTE_AE_IP')
REMOTE_AE_PORT = config.getint('dicom', 'REMOTE_AE_PORT')
# Get database path from config or use default
DB_FILE = config.get('general', 'XRAYVISION_DB_PATH', fallback='xrayvision.db')
def db_check_study_exists(study_instance_uid):
"""
Check if a study with the given Study Instance UID exists in the database.
Args:
study_instance_uid (str): Study Instance UID to check
Returns:
bool: True if study exists in database, False otherwise
"""
try:
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM exams WHERE study = ?", (study_instance_uid,))
count = cursor.fetchone()[0]
return count > 0
except Exception as e:
logging.error(f"Error checking database for study {study_instance_uid}: {e}")
return False
def send_c_move(ae, peer_ae, peer_ip, peer_port, study_instance_uid):
"""
Send a C-MOVE request to retrieve a study from a remote DICOM server.
This function establishes a DICOM association with a remote PACS and sends
a C-MOVE request to have a specific study (identified by Study Instance UID)
sent to our local AE.
Args:
ae (AE): Local Application Entity
peer_ae (str): Remote AE title
peer_ip (str): Remote AE IP address
peer_port (int): Remote AE port
study_instance_uid (str): Study Instance UID to retrieve
Returns:
None
"""
# Create the association
assoc = ae.associate(peer_ip, peer_port, ae_title = peer_ae)
if assoc.is_established:
# The retrieval dataset
ds = Dataset()
ds.QueryRetrieveLevel = "STUDY"
ds.StudyInstanceUID = study_instance_uid
# Get the response
responses = [(None, None)]
try:
responses = assoc.send_c_move(ds, ae.ae_title, PatientRootQueryRetrieveInformationModelMove)
except Exception as e:
logging.error(f"Error in C-MOVE: {e}")
for (move_status, _) in responses:
if move_status:
logging.info(f"C-MOVE for {study_instance_uid} returned status: 0x{move_status.Status:04X}")
# Release the association
assoc.release()
else:
logging.error("Could not establish C-MOVE association.")
def query_retrieve_cr_studies(local_ae, peer_ae, peer_ip, peer_port, year, month, day = None):
"""
Query and retrieve CR studies for a specified date range.
This function queries a remote DICOM PACS for CR studies for either a full month
or a specific day, and requests each found study to be sent to the local AE.
It processes one day at a time with appropriate delays between requests.
Args:
local_ae (str): Local AE title
peer_ae (str): Remote AE title
peer_ip (str): Remote AE IP address
peer_port (int): Remote AE port
year (int): Year to query
month (int): Month to query (1-12)
day (int, optional): Specific day to query (1-31). If None, queries entire month.
Returns:
None
"""
ae = AE(ae_title = local_ae)
ae.requested_contexts = QueryRetrievePresentationContexts
ae.connection_timeout = 30
# Date range
if day is None:
start_date = datetime(year, month, 1)
end_date = (start_date + timedelta(days = 32)).replace(day = 1)
else:
start_date = datetime(year, month, day)
end_date = (start_date + timedelta(days = 1))
# Process each day separately
for day in range((end_date - start_date).days):
date = (start_date + timedelta(days=day)).strftime("%Y%m%d")
logging.info(f"Query studies for {date}.")
# The query dataset
ds = Dataset()
ds.QueryRetrieveLevel = "STUDY"
ds.Modality = "CR"
ds.StudyDate = date
# Create the association
assoc = ae.associate(peer_ip, peer_port, ae_title = peer_ae)
if assoc.is_established:
# Get the responses list
responses = [(None, None)]
try:
responses = assoc.send_c_find(ds, PatientRootQueryRetrieveInformationModelFind)
except Exception as e:
logging.error(f"Error in C-FIND: {e}")
for (status, identifier) in responses:
if status and status.Status in (0xFF00, 0xFF01):
study_instance_uid = identifier.StudyInstanceUID
# Check if study already exists in database
if db_check_study_exists(study_instance_uid):
logging.info(f"[{date}] Skipping Study UID: {study_instance_uid} (already in database)")
else:
logging.info(f"[{date}] Queued Study UID: {study_instance_uid}")
send_c_move(ae, peer_ae, peer_ip, peer_port, study_instance_uid)
# Sleep
time.sleep(1)
# Release the association
assoc.release()
else:
logging.warning(f"Association failed for {date}.")
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description = "Run monthly CR Query/Retrieve")
parser.add_argument("--day", type = int, help = "Day number (1-31)")
parser.add_argument("--month", type = int, required = True, help = "Month number (1-12)")
parser.add_argument("--year", type = int, required = True, help = "Year (e.g. 2025)")
parser.add_argument("--ae", default = AE_TITLE, help = "Local AE Title")
parser.add_argument("--peer-ae", default = REMOTE_AE_TITLE, help = "Peer AE Title")
parser.add_argument("--peer-ip", default = REMOTE_AE_IP, help = "Peer IP address")
parser.add_argument("--peer-port", type = int, default = REMOTE_AE_PORT, help = "Peer port")
args = parser.parse_args()
query_retrieve_cr_studies(
local_ae = args.ae,
peer_ae = args.peer_ae,
peer_ip = args.peer_ip,
peer_port = args.peer_port,
year = args.year,
month = args.month,
day = args.day
)
except KeyboardInterrupt:
logging.info("QR utility stopped by user.")
except Exception as e:
logging.error(f"QR utility error: {e}")
finally:
logging.info("QR utility shutdown complete.")