forked from simonmd/couchdicom
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcouchdicom.rb
executable file
·228 lines (201 loc) · 6.97 KB
/
couchdicom.rb
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
222
223
224
225
226
227
228
#!/usr/bin/env ruby
# Modules required:
require 'rubygems'
require 'find'
require 'couchrest'
require 'couchrest_model'
require 'dicom'
require 'rmagick'
require 'optparse'
include DICOM
options = {}
option_parser = OptionParser.new do |opts|
executable_name = File.basename($PROGRAM_NAME)
opts.banner = "Load the DICOM metadata contained in a folder in a COUCHDB database
Usage: ./#{executable_name} [options]
"
opts.on("-a","--attachments", "Switch to upload DICOM pixeldata as attachments") do
options[:attachments] = true
end
opts.on("-j","--jpg_attachments", "Switch to upload WADO objects as attachments") do
options[:jpg_attachments] = true
end
opts.on("-f FOLDER", "Define the directory to be read") do |folder|
options[:folder] = folder
end
opts.on("-t JPG_FOLDER", "Define the directory where temporary JPEGS should be stored") do |jpg_folder|
options[:jpg_folder] = jpg_folder
end
opts.on("-d DB_URL", "Define Database URL") do |db_url|
options[:db_url] = db_url
end
end
option_parser.parse!
# Constants
if options[:folder]
DIRS = [options[:folder]]
else
DIRS = ["/Users/simonmd/Desktop/DATASETS/BOUVIER"]
end
if options[:jpg_folder]
JPGDIR = options[:jpg_folder]
else
JPGDIR = "/Users/simonmd/Desktop/WADOS"
end
if options[:db_url]
DBURL = options[:db_url]
else
DBURL = "http://admin:admin@localhost:5984/couchdicom"
end
DB_BULK_SAVE_CACHE_LIMIT = 500 # Define Bulk save cache limit
dicom_attachment = false # Define if DICOM files should be attached inside the CouchDB document
jpeg_attachment = false # Define if JPEG files should be attached inside the CouchDB document (eg. for serving as WADO)
# Intialize logger
log = Logger.new('couchdicom_import.log')
log.level = Logger::WARN
log.debug("Created logger")
log.info("Program started")
DICOM.logger = log
DICOM.logger.level = Logger::DEBUG
# Create CouchDB database if it doesn't already exist
DB = CouchRest.database!(DBURL)
# Set the limit of documents for bulk updating
DB.bulk_save_cache_limit = DB_BULK_SAVE_CACHE_LIMIT
# Class to generate a CouchDB extended document
class Dicomdoc < CouchRest::Model::Base
mass_assign_any_attribute true
use_database DB
unique_id :slug
property :slug, :read_only => true
property :docuid
timestamps!
set_callback :save, :before, :generate_slug_from_docuid
def generate_slug_from_docuid
self['slug'] = docuid if new?
end
end
# Returns an element's key (or in case of Item, it's index).
def extract_key(element)
if element.is_a?(Item)
# Use the Item's index instead of tag:
cdbkey = element.index
else
# Read tag as CouchDB Key:
cdbkey = element.tag
# Remove the comma from the tag string:
cdbkey = cdbkey.gsub(",","")
# Prepend a 't' for easier javascript map/reduce functions:
cdbkey = "t" + cdbkey
end
return cdbkey
end
# Returns a data element's value.
# Note that data elements of vr OB & OW have no value decoded, so value returned for these will be nil.
def extract_value(element)
# Read value as CouchDB value for that key:
cdbvalue = element.value
# Convert encoding to UTF-8 with Ruby 1.9 'encode' method
cdbvalue = cdbvalue.encode('utf-8', 'iso-8859-1') if cdbvalue.class == String
return cdbvalue
end
# Returns a hash with tag/data-element-value (or child elements) are used as key/value.
def process_children(parent_element)
h = Hash.new
# Iterate over all children and repeat recursively for any child which is parent:
parent_element.children.each do |element|
if element.children?
value = process_children(element)
key = extract_key(element)
elsif element.is_a?(Element)
key = extract_key(element)
value = extract_value(element)
end
# Only write key-value pair if the value is not empty (to conserve space)
unless value.blank?
h[key] = value
end
end
return h
end
# Discover all the files contained in the specified directory and all its sub-directories:
excludes = ['DICOMDIR']
files = Array.new()
for dir in DIRS
Find.find(dir) do |path|
if FileTest.directory?(path)
if excludes.include?(File.basename(path))
Find.prune # Don't look any further into this directory.
else
next
end
else
files += [path] # Store the file in our array
end
end
end
# Start total timer
total_start_time = Time.now
# Use a loop to run through all the files, reading its data and transferring it to the database.
files.each_index do |i|
iteration_start_time = Time.now
# Read the file:
log.info("Attempting to read file #{i} ...")
dcm = DObject.read(files[i])
# If the file was read successfully as a DICOM file, go ahead and extract content:
if dcm.read_success
log.info("Successfully read file #{files[i]}")
# Extract a hash of with tag/data-element-value as key/value:
h = process_children(dcm)
# Save filepath in hash
h["filepath"] = files[i]
# Read in the dicom file as a 'file' object
file = File.new(files[i])
# Create new CouchDB document with the generated hash
currentdicom = Dicomdoc.new(h)
# Set the document ID to the Instance Unique ID (UID)
currentdicom.docuid = h["t00080018"].to_s
# Check if DICOM attachment is selected
if options[:attachments] == true
# Create the attachment from the actual dicom file
currentdicom.create_attachment({:name => 'imagedata', :file => file, :content_type => 'application/dicom'})
end
# Check if JPEG attachment is selected
if options[:jpg_attachments] == true
# Load pixel data to ImageMagick class
log.info("Attempting to load pixel data for file #{files[i]} ...")
if dcm.image
log.info("Pixel data for file #{files[i]} read successfully")
image = dcm.image.normalize
# Save pixeldata as jpeg image in wado cache directory
wadoimg_path = "#{JPGDIR}/wado-#{currentdicom.docuid}.jpg"
# Write the jpeg for WADO
image.write(wadoimg_path)
# Read to insert in attachment (SURELY this can be done directly)
wadofile = File.new(wadoimg_path)
# Create an attachment from the created jpeg
currentdicom.create_attachment({:name => 'wadojpg', :file => wadofile, :content_type => 'image/jpeg'})
else
log.warn("could not read pixel data for file #{files[i]} ...")
end
end
# Save the CouchDB document
begin
currentdicom.save
# Uncomment if bulk saving is desired (Little performance gain, bottleneck is in dicom reads)
# currentdicom.save(bulk = true)
# If an error ocurrs, raise exception and log it
rescue Exception => exc
log.warn("Could not save file #{files[i]} to database; Error: #{exc.message}")
end
end
# Log processing time for the file
iteration_end_time = Time.now
iterationtime = iteration_end_time - iteration_start_time
log.info("Iteration time for file #{i} finished in #{iterationtime} s")
end
# Log total processing time
total_end_time = Time.now
totaltime = total_end_time - total_start_time
log.info("Full processing time: #{totaltime} seconds")
# Close the logger
log.close