rtkit 0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +10 -0
- data/COPYING +674 -0
- data/README.rdoc +107 -0
- data/lib/rtkit.rb +68 -0
- data/lib/rtkit/beam.rb +346 -0
- data/lib/rtkit/bin_image.rb +578 -0
- data/lib/rtkit/bin_matcher.rb +241 -0
- data/lib/rtkit/bin_volume.rb +263 -0
- data/lib/rtkit/collimator.rb +157 -0
- data/lib/rtkit/collimator_setup.rb +143 -0
- data/lib/rtkit/constants.rb +215 -0
- data/lib/rtkit/contour.rb +213 -0
- data/lib/rtkit/control_point.rb +371 -0
- data/lib/rtkit/coordinate.rb +83 -0
- data/lib/rtkit/data_set.rb +264 -0
- data/lib/rtkit/dose.rb +70 -0
- data/lib/rtkit/dose_distribution.rb +206 -0
- data/lib/rtkit/dose_volume.rb +280 -0
- data/lib/rtkit/frame.rb +164 -0
- data/lib/rtkit/image.rb +372 -0
- data/lib/rtkit/image_series.rb +290 -0
- data/lib/rtkit/logging.rb +158 -0
- data/lib/rtkit/methods.rb +105 -0
- data/lib/rtkit/mixins/image_parent.rb +40 -0
- data/lib/rtkit/patient.rb +229 -0
- data/lib/rtkit/pixel_data.rb +237 -0
- data/lib/rtkit/plan.rb +259 -0
- data/lib/rtkit/plane.rb +165 -0
- data/lib/rtkit/roi.rb +388 -0
- data/lib/rtkit/rt_dose.rb +237 -0
- data/lib/rtkit/rt_image.rb +179 -0
- data/lib/rtkit/ruby_extensions.rb +165 -0
- data/lib/rtkit/selection.rb +189 -0
- data/lib/rtkit/series.rb +77 -0
- data/lib/rtkit/setup.rb +198 -0
- data/lib/rtkit/slice.rb +184 -0
- data/lib/rtkit/staple.rb +305 -0
- data/lib/rtkit/structure_set.rb +442 -0
- data/lib/rtkit/study.rb +214 -0
- data/lib/rtkit/variables.rb +23 -0
- data/lib/rtkit/version.rb +6 -0
- metadata +159 -0
@@ -0,0 +1,290 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# The ImageSeries class contains methods that are specific for the slice based image modalites (e.g. CT, MR).
|
4
|
+
#
|
5
|
+
# === Inheritance
|
6
|
+
#
|
7
|
+
# * ImageSeries inherits all methods and attributes from the Series class.
|
8
|
+
#
|
9
|
+
class ImageSeries < Series
|
10
|
+
|
11
|
+
include ImageParent
|
12
|
+
|
13
|
+
# The Frame (of Reference) which this ImageSeries belongs to.
|
14
|
+
attr_accessor :frame
|
15
|
+
# An array of Image references.
|
16
|
+
attr_reader :images
|
17
|
+
# A hash containing SOP Instance UIDs as key and Slice Positions as value.
|
18
|
+
attr_reader :slices
|
19
|
+
# A hash containing Slice Positions as key and SOP Instance UIDS as value.
|
20
|
+
attr_accessor :sop_uids
|
21
|
+
# An array of Structure Sets associated with this Image Series.
|
22
|
+
attr_reader :structs
|
23
|
+
|
24
|
+
# Creates a new ImageSeries instance by loading series information from the specified DICOM object.
|
25
|
+
# The Series' UID string value is used to uniquely identify an ImageSeries.
|
26
|
+
#
|
27
|
+
# === Parameters
|
28
|
+
#
|
29
|
+
# * <tt>dcm</tt> -- An instance of a DICOM object (DICOM::DObject) with an image type modality (e.g. CT or MR).
|
30
|
+
# * <tt>study</tt> -- The Study instance that this ImageSeries belongs to.
|
31
|
+
#
|
32
|
+
def self.load(dcm, study)
|
33
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
34
|
+
raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
|
35
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with an Image Series type modality, got #{dcm.value(MODALITY)}." unless IMAGE_SERIES.include?(dcm.value(MODALITY))
|
36
|
+
# Required attributes:
|
37
|
+
modality = dcm.value(MODALITY)
|
38
|
+
series_uid = dcm.value(SERIES_UID)
|
39
|
+
# Optional attributes:
|
40
|
+
class_uid = dcm.value(SOP_CLASS)
|
41
|
+
date = dcm.value(SERIES_DATE)
|
42
|
+
time = dcm.value(SERIES_TIME)
|
43
|
+
description = dcm.value(SERIES_DESCR)
|
44
|
+
# Check if a Frame with the given UID already exists, and if not, create one:
|
45
|
+
frame = study.patient.dataset.frame(dcm.value(FRAME_OF_REF)) || frame = study.patient.create_frame(dcm.value(FRAME_OF_REF), dcm.value(POS_REF_INDICATOR))
|
46
|
+
# Create the ImageSeries instance:
|
47
|
+
is = self.new(series_uid, modality, frame, study, :class_uid => class_uid, :date => date, :time => time, :description => description)
|
48
|
+
is.add(dcm)
|
49
|
+
# Add our ImageSeries instance to its corresponding Frame:
|
50
|
+
frame.add_series(is)
|
51
|
+
return is
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates a new ImageSeries instance.
|
55
|
+
#
|
56
|
+
# === Parameters
|
57
|
+
#
|
58
|
+
# * <tt>series_uid</tt> -- The Series Instance UID string.
|
59
|
+
# * <tt>modality</tt> -- The Modality string of the ImageSeries, e.g. 'CT' or 'MR'.
|
60
|
+
# * <tt>frame</tt> -- The Frame instance that this ImageSeries belongs to.
|
61
|
+
# * <tt>study</tt> -- The Study instance that this ImageSeries belongs to.
|
62
|
+
# * <tt>options</tt> -- A hash of parameters.
|
63
|
+
#
|
64
|
+
# === Options
|
65
|
+
#
|
66
|
+
# * <tt>:class_uid</tt> -- String. The SOP Class UID (DICOM tag '0008,0016').
|
67
|
+
# * <tt>:date</tt> -- String. The Series Date (DICOM tag '0008,0021').
|
68
|
+
# * <tt>:time</tt> -- String. The Series Time (DICOM tag '0008,0031').
|
69
|
+
# * <tt>:description</tt> -- String. The Series Description (DICOM tag '0008,103E').
|
70
|
+
#
|
71
|
+
def initialize(series_uid, modality, frame, study, options={})
|
72
|
+
raise ArgumentError, "Invalid argument 'series_uid'. Expected String, got #{series_uid.class}." unless series_uid.is_a?(String)
|
73
|
+
raise ArgumentError, "Invalid argument 'modality'. Expected String, got #{modality.class}." unless modality.is_a?(String)
|
74
|
+
raise ArgumentError, "Invalid argument 'frame'. Expected Frame, got #{frame.class}." unless frame.is_a?(Frame)
|
75
|
+
raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
|
76
|
+
raise ArgumentError, "Invalid argument 'modality'. Expected an Image Series type modality, got #{modality}." unless IMAGE_SERIES.include?(modality)
|
77
|
+
# Pass attributes to Series initialization:
|
78
|
+
super(series_uid, modality, study, options)
|
79
|
+
# Key attributes:
|
80
|
+
@frame = frame
|
81
|
+
# Default attributes:
|
82
|
+
@slices = Hash.new
|
83
|
+
@sop_uids = Hash.new
|
84
|
+
@images = Array.new
|
85
|
+
@structs = Array.new
|
86
|
+
@image_positions = Hash.new
|
87
|
+
# A hash with the associated StructureSet's UID as key and the instance of the StructureSet that belongs to this ImageSeries as value:
|
88
|
+
@associated_structs = Hash.new
|
89
|
+
# Register ourselves with the study & frame:
|
90
|
+
@study.add_series(self)
|
91
|
+
@frame.add_series(self)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
95
|
+
#
|
96
|
+
def ==(other)
|
97
|
+
if other.respond_to?(:to_image_series)
|
98
|
+
other.send(:state) == state
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
alias_method :eql?, :==
|
103
|
+
|
104
|
+
# Adds a DICOM object (Image) to the ImageSeries, by creating a new Image instance linked to this ImageSeries.
|
105
|
+
#
|
106
|
+
def add(dcm)
|
107
|
+
Image.load(dcm, self)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Adds an Image to this ImageSeries.
|
111
|
+
#
|
112
|
+
def add_image(image)
|
113
|
+
raise ArgumentError, "Invalid argument 'image'. Expected Image, got #{image.class}." unless image.is_a?(Image)
|
114
|
+
@images << image unless @frame.image(image.uid)
|
115
|
+
@slices[image.uid] = image.pos_slice
|
116
|
+
@sop_uids[image.pos_slice] = image.uid
|
117
|
+
# The link between image uid and image instance is kept in the Frame, instead of the ImageSeries:
|
118
|
+
@frame.add_image(image) unless @frame.image(image.uid)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Adds a StructureSet to this ImageSeries.
|
122
|
+
#
|
123
|
+
def add_struct(struct)
|
124
|
+
raise ArgumentError, "Invalid argument 'struct'. Expected StructureSet, got #{struct.class}." unless struct.is_a?(StructureSet)
|
125
|
+
# Do not add it again if the struct already belongs to this instance:
|
126
|
+
@structs << struct unless @associated_structs[struct.uid]
|
127
|
+
@associated_structs[struct.uid] = struct
|
128
|
+
end
|
129
|
+
|
130
|
+
=begin
|
131
|
+
# Returns the array position in the sorted array of slices that is closest to the provided slice.
|
132
|
+
# If slice value is out of bounds (it is further from boundaries than the slice interval), false is returned.
|
133
|
+
def corresponding_slice(slice, slices)
|
134
|
+
above_pos = (0...slices.length).select{|x| slices[x]>=slice}.first
|
135
|
+
below_pos = (0...slices.length).select{|x| slices[x]<=slice}.last
|
136
|
+
# With Ruby 1.9 this can supposedly be simplified to: below_pos = slices.index{|x| x<=slice}
|
137
|
+
# Exact match or between two slices?
|
138
|
+
if above_pos == below_pos
|
139
|
+
# Exact match (both point to the same index).
|
140
|
+
slice_index = above_pos
|
141
|
+
else
|
142
|
+
# Value in between. Return the index of the value that is closest to our value.
|
143
|
+
if (slice-slices[above_pos]).abs < (slice-slices[below_pos]).abs
|
144
|
+
slice_index = above_pos
|
145
|
+
else
|
146
|
+
slice_index = below_pos
|
147
|
+
end
|
148
|
+
end
|
149
|
+
return slice_index
|
150
|
+
end
|
151
|
+
=end
|
152
|
+
|
153
|
+
# Generates a Fixnum hash value for this instance.
|
154
|
+
#
|
155
|
+
def hash
|
156
|
+
state.hash
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns the Image instance mathcing the specified SOP Instance UID (if an argument is used).
|
160
|
+
# If a specified UID doesn't match, nil is returned.
|
161
|
+
# If no argument is passed, the first Image instance associated with the ImageSeries is returned.
|
162
|
+
#
|
163
|
+
# === Parameters
|
164
|
+
#
|
165
|
+
# * <tt>uid</tt> -- String. The value of the SOP Instance UID element of the Image.
|
166
|
+
#
|
167
|
+
def image(*args)
|
168
|
+
raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
|
169
|
+
if args.length == 1
|
170
|
+
if args.first.is_a?(Float)
|
171
|
+
# Presumably an image position:
|
172
|
+
return @image_positions[args.first]
|
173
|
+
else
|
174
|
+
# Presumably a uid string:
|
175
|
+
return @frame.image(args.first)
|
176
|
+
end
|
177
|
+
else
|
178
|
+
# No argument used, therefore we return the first Image instance:
|
179
|
+
return @images.first
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Analyses the Image instances belonging to this ImageSeries to determine
|
184
|
+
# if there is an Image which matches the specified Plane.
|
185
|
+
# Returns the Image if a match is found, nil if not.
|
186
|
+
#
|
187
|
+
# === Parameters
|
188
|
+
#
|
189
|
+
# * <tt>plane</tt> -- The Plane instance which images will be matched against.
|
190
|
+
#
|
191
|
+
def match_image(plane)
|
192
|
+
raise ArgumentError, "Invalid argument 'plane'. Expected Plane, got #{plane.class}." unless plane.is_a?(Plane)
|
193
|
+
matching_image = nil
|
194
|
+
planes_in_series = Array.new
|
195
|
+
@images.each do |image|
|
196
|
+
# Get three coordinates from the image:
|
197
|
+
col_indices = NArray.to_na([0,image.columns/2, image.columns-1])
|
198
|
+
row_indices = NArray.to_na([image.rows/2, image.rows-1, 0])
|
199
|
+
x, y, z = image.coordinates_from_indices(col_indices, row_indices)
|
200
|
+
coordinates = Array.new
|
201
|
+
x.length.times do |i|
|
202
|
+
coordinates << Coordinate.new(x[i], y[i], z[i])
|
203
|
+
end
|
204
|
+
# Determine the image plane:
|
205
|
+
planes_in_series << Plane.calculate(coordinates[0], coordinates[1], coordinates[2])
|
206
|
+
end
|
207
|
+
# Search for a match amongst the planes of this series:
|
208
|
+
index = plane.match(planes_in_series)
|
209
|
+
matching_image = @images[index] if index
|
210
|
+
return matching_image
|
211
|
+
end
|
212
|
+
|
213
|
+
# Returns all ROIs having the same Frame of Reference as this
|
214
|
+
# image series from the structure set(s) belonging to this series.
|
215
|
+
# Returns the ROIs in an Array. If no ROIs are matched, an empty array is returned.
|
216
|
+
#
|
217
|
+
def rois
|
218
|
+
frame_rois = Array.new
|
219
|
+
structs.each do |struct|
|
220
|
+
frame_rois << struct.rois_in_frame(@frame.uid)
|
221
|
+
end
|
222
|
+
return frame_rois.flatten
|
223
|
+
end
|
224
|
+
|
225
|
+
# Sets the resolution of all images in this image series.
|
226
|
+
# The images will either be expanded or cropped depending on whether
|
227
|
+
# the specified resolution is bigger or smaller than the existing one.
|
228
|
+
#
|
229
|
+
# === Parameters
|
230
|
+
#
|
231
|
+
# * <tt>columns</tt> -- Integer. The number of columns applied to the cropped/expanded image series.
|
232
|
+
# * <tt>rows</tt> -- Integer. The number of rows applied to the cropped/expanded image series.
|
233
|
+
#
|
234
|
+
# === Options
|
235
|
+
#
|
236
|
+
# * <tt>:hor</tt> -- Symbol. The side (in the horisontal image direction) to apply the crop/border (:left, :right or :even (default)).
|
237
|
+
# * <tt>:ver</tt> -- Symbol. The side (in the vertical image direction) to apply the crop/border (:bottom, :top or :even (default)).
|
238
|
+
#
|
239
|
+
def set_resolution(columns, rows, options={})
|
240
|
+
@images.each do |img|
|
241
|
+
img.set_resolution(columns, rows, options)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Returns the StructureSet instance mathcing the specified SOP Instance UID (if an argument is used).
|
246
|
+
# If a specified UID doesn't match, nil is returned.
|
247
|
+
# If no argument is passed, the first StructureSet instance associated with the ImageSeries is returned.
|
248
|
+
#
|
249
|
+
# === Parameters
|
250
|
+
#
|
251
|
+
# * <tt>uid</tt> -- String. The value of the SOP Instance UID element.
|
252
|
+
#
|
253
|
+
def struct(*args)
|
254
|
+
raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
|
255
|
+
if args.length == 1
|
256
|
+
raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
|
257
|
+
return @associated_structs[args.first]
|
258
|
+
else
|
259
|
+
# No argument used, therefore we return the first StructureSet instance:
|
260
|
+
return @structs.first
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Returns self.
|
265
|
+
#
|
266
|
+
def to_image_series
|
267
|
+
self
|
268
|
+
end
|
269
|
+
|
270
|
+
# Writes all images in this image series to DICOM files in the specified folder.
|
271
|
+
# The file names are set by the image's UID string, followed by a '.dcm' extension.
|
272
|
+
#
|
273
|
+
def write(path)
|
274
|
+
@images.each do |img|
|
275
|
+
img.write(path + img.uid + '.dcm')
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
|
280
|
+
private
|
281
|
+
|
282
|
+
|
283
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
284
|
+
#
|
285
|
+
def state
|
286
|
+
[@images, @series_uid, @structs]
|
287
|
+
end
|
288
|
+
|
289
|
+
end
|
290
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# This module handles logging functionality.
|
4
|
+
#
|
5
|
+
# Logging functionality uses the Standard library's Logger class.
|
6
|
+
# To properly handle progname, which inside the RTKIT module is simply
|
7
|
+
# "RTKIT", in all cases, we use an implementation with a proxy class.
|
8
|
+
#
|
9
|
+
# === Examples
|
10
|
+
#
|
11
|
+
# require 'rtkit'
|
12
|
+
# include RTKIT
|
13
|
+
#
|
14
|
+
# # Logging to STDOUT with DEBUG level:
|
15
|
+
# RTKIT.logger = Logger.new(STDOUT)
|
16
|
+
# RTKIT.logger.level = Logger::DEBUG
|
17
|
+
#
|
18
|
+
# # Logging to a file:
|
19
|
+
# RTKIT.logger = Logger.new('my_logfile.log')
|
20
|
+
#
|
21
|
+
# # Combine an external logger with RTKIT:
|
22
|
+
# logger = Logger.new(STDOUT)
|
23
|
+
# logger.progname = "MY_APP"
|
24
|
+
# RTKIT.logger = logger
|
25
|
+
# # Now you can call the logger in the following ways:
|
26
|
+
# RTKIT.logger.info "Message" # => "RTKIT: Message"
|
27
|
+
# RTKIT.logger.info("MY_MODULE) {"Message"} # => "MY_MODULE: Message"
|
28
|
+
# logger.info "Message" # => "MY_APP: Message"
|
29
|
+
#
|
30
|
+
# For more information, please read the Standard library Logger documentation.
|
31
|
+
#
|
32
|
+
module Logging
|
33
|
+
|
34
|
+
require 'logger'
|
35
|
+
|
36
|
+
# Inclusion hook to make the ClassMethods available to whatever
|
37
|
+
# includes the Logging module, i.e. the RTKIT module.
|
38
|
+
#
|
39
|
+
def self.included(base)
|
40
|
+
base.extend(ClassMethods)
|
41
|
+
end
|
42
|
+
|
43
|
+
module ClassMethods
|
44
|
+
|
45
|
+
# We use our own ProxyLogger to achieve the features wanted for RTKIT logging,
|
46
|
+
# e.g. using RTKIT as progname for messages logged within the RTKIT module
|
47
|
+
# (for both the Standard logger as well as the Rails logger), while still allowing
|
48
|
+
# a custom progname to be used when the logger is called outside the RTKIT module.
|
49
|
+
#
|
50
|
+
class ProxyLogger
|
51
|
+
|
52
|
+
# Creating the ProxyLogger instance.
|
53
|
+
#
|
54
|
+
# === Parameters
|
55
|
+
#
|
56
|
+
# * <tt>target</tt> -- A Logger instance (e.g. Standard Logger or ActiveSupport::BufferedLogger).
|
57
|
+
#
|
58
|
+
def initialize(target)
|
59
|
+
@target = target
|
60
|
+
end
|
61
|
+
|
62
|
+
# Catches missing methods.
|
63
|
+
# In our case, the methods of interest are the typical logger methods,
|
64
|
+
# i.e. log, info, fatal, error, debug, where the arguments/block are
|
65
|
+
# redirected to the logger in a specific way so that our stated logger
|
66
|
+
# features are achieved (this behaviour depends on the logger
|
67
|
+
# (Rails vs Standard) and in the case of Standard logger,
|
68
|
+
# whether or not a block is given).
|
69
|
+
#
|
70
|
+
# === Examples
|
71
|
+
#
|
72
|
+
# # Inside the RTKIT module or an external class with 'include RTKIT::Logging':
|
73
|
+
# logger.info "message"
|
74
|
+
#
|
75
|
+
# # Calling from outside the RTKIT module:
|
76
|
+
# RTKIT.logger.info "message"
|
77
|
+
#
|
78
|
+
def method_missing(method_name, *args, &block)
|
79
|
+
if method_name.to_s =~ /(log|debug|info|warn|error|fatal)/
|
80
|
+
# Rails uses it's own buffered logger which does not
|
81
|
+
# work with progname + block as the standard logger does:
|
82
|
+
if defined?(Rails)
|
83
|
+
@target.send(method_name, "RTKIT: #{args.first}")
|
84
|
+
elsif block_given?
|
85
|
+
@target.send(method_name, *args) { yield }
|
86
|
+
else
|
87
|
+
@target.send(method_name, "RTKIT") { args.first }
|
88
|
+
end
|
89
|
+
else
|
90
|
+
@target.send(method_name, *args, &block)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
# The logger class variable (must be initialized
|
97
|
+
# before it is referenced by the object setter).
|
98
|
+
#
|
99
|
+
@@logger = nil
|
100
|
+
|
101
|
+
# The logger object setter.
|
102
|
+
# This method is used to replace the default logger instance with
|
103
|
+
# a custom logger of your own.
|
104
|
+
#
|
105
|
+
# === Parameters
|
106
|
+
#
|
107
|
+
# * <tt>l</tt> -- A Logger instance (e.g. a custom standard Logger).
|
108
|
+
#
|
109
|
+
# === Examples
|
110
|
+
#
|
111
|
+
# # Create a logger which ages logfile once it reaches a certain size,
|
112
|
+
# # leaves 10 "old log files" with each file being about 1,024,000 bytes:
|
113
|
+
# RTKIT.logger = Logger.new('foo.log', 10, 1024000)
|
114
|
+
#
|
115
|
+
def logger=(l)
|
116
|
+
@@logger = ProxyLogger.new(l)
|
117
|
+
end
|
118
|
+
|
119
|
+
# The logger object getter.
|
120
|
+
# Returns the logger class variable, if defined.
|
121
|
+
# If not defined, sets up the Rails logger (if in a Rails environment),
|
122
|
+
# or a Standard logger if not.
|
123
|
+
#
|
124
|
+
# === Examples
|
125
|
+
#
|
126
|
+
# # Inside the RTKIT module (or a class with 'include RTKIT::Logging'):
|
127
|
+
# logger # => Logger instance
|
128
|
+
#
|
129
|
+
# # Accessing from outside the RTKIT module:
|
130
|
+
# RTKIT.logger # => Logger instance
|
131
|
+
#
|
132
|
+
def logger
|
133
|
+
@@logger ||= lambda {
|
134
|
+
if defined?(Rails)
|
135
|
+
ProxyLogger.new(Rails.logger)
|
136
|
+
else
|
137
|
+
l = Logger.new(STDOUT)
|
138
|
+
l.level = Logger::INFO
|
139
|
+
ProxyLogger.new(l)
|
140
|
+
end
|
141
|
+
}.call
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
# A logger object getter.
|
147
|
+
# Forwards the call to the logger class method of the Logging module.
|
148
|
+
#
|
149
|
+
def logger
|
150
|
+
self.class.logger
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
# Include the Logging module so we can use RTKIT.logger.
|
156
|
+
include Logging
|
157
|
+
|
158
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
class << self
|
4
|
+
|
5
|
+
#--
|
6
|
+
# Module methods:
|
7
|
+
#++
|
8
|
+
|
9
|
+
# Finds all files contained in the specified folder or folders (including any sub-folders).
|
10
|
+
# Returns an array containing the discovered file strings.
|
11
|
+
#
|
12
|
+
# === Parameters
|
13
|
+
#
|
14
|
+
# * <tt>path_or_paths</tt> -- String or Array of strings. The path(s) in which to find all files.
|
15
|
+
#
|
16
|
+
def files(path_or_paths)
|
17
|
+
raise ArgumentError, "Invalid argument 'path_or_paths'. Expected String or Array, got #{paths.class}." unless [String, Array].include?(path_or_paths.class)
|
18
|
+
raise ArgumentError, "Invalid argument 'path_or_paths'. Expected Array to contain only strings, got #{path_or_paths.collect{|p| p.class}.uniq}." if path_or_paths.is_a?(Array) && path_or_paths.collect{|p| p.class}.uniq != [String]
|
19
|
+
paths = path_or_paths.is_a?(Array) ? path_or_paths : [path_or_paths]
|
20
|
+
files = Array.new
|
21
|
+
# Iterate the folders (and their subfolders) to extract all files:
|
22
|
+
for dir in paths
|
23
|
+
Find.find(dir) do |path|
|
24
|
+
if FileTest.directory?(path)
|
25
|
+
next
|
26
|
+
else
|
27
|
+
# Store the file in our array:
|
28
|
+
files << path
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
return files
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generates and returns a random Frame Instance UID string.
|
36
|
+
#
|
37
|
+
def frame_uid
|
38
|
+
return self.generate_uids('9').first
|
39
|
+
end
|
40
|
+
|
41
|
+
# Generates one or several random UID strings.
|
42
|
+
# The UIDs are based on the RTKIT dicom_root attribute, a type prefix, a datetime part,
|
43
|
+
# a random number part, and an index part (when multiple UIDs are requested,
|
44
|
+
# e.g. for a SOP Instances in a Series).
|
45
|
+
# Returns the UIDs in a string array.
|
46
|
+
#
|
47
|
+
# === Parameters
|
48
|
+
#
|
49
|
+
# * <tt>prefix</tt> -- String. A (numerical) type string which sits between the dicom root and the random part of the UID.
|
50
|
+
# * <tt>instances</tt> -- Integer. The number of UIDs to generate. Defaults to 1.
|
51
|
+
#
|
52
|
+
def generate_uids(prefix, instances=1)
|
53
|
+
raise ArgumentError, "Invalid argument 'prefix'. Expected (integer) String, got #{prefix.class}." unless prefix.is_a?(String)
|
54
|
+
raise ArgumentError, "Invalid argument 'instances'. Expected Integer (when defined), got #{instances.class}." if instances && !instances.is_a?(Integer)
|
55
|
+
raise ArgumentError, "Invalid argument 'prefix'. Expected non-zero Integer (String), got #{prefix}." if prefix.to_i == 0
|
56
|
+
raise ArgumentError, "Invalid argument 'instances'. Expected positive Integer (when defined), got #{instances}." if instances && instances < 0
|
57
|
+
prefix = prefix.to_i
|
58
|
+
# NB! For UIDs, leading zeroes after a dot is not allowed, and must be removed:
|
59
|
+
date = Time.now.strftime("%Y%m%d").to_i.to_s
|
60
|
+
time = Time.now.strftime("%H%M%S").to_i.to_s
|
61
|
+
random = rand(99999) + 1 # (Minimum 1, max. 99999)
|
62
|
+
base_uid = [RTKIT.dicom_root, prefix, date, time, random].join('.')
|
63
|
+
uids = Array.new
|
64
|
+
if instances == 1
|
65
|
+
uids << base_uid
|
66
|
+
else
|
67
|
+
(1..instances).to_a.each do |i|
|
68
|
+
uids << "#{base_uid}.#{i}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
return uids
|
72
|
+
end
|
73
|
+
|
74
|
+
# Generates and returns a random Series Instance UID string.
|
75
|
+
#
|
76
|
+
def series_uid
|
77
|
+
return self.generate_uids('2').first
|
78
|
+
end
|
79
|
+
|
80
|
+
# Generates and returns a random SOP Instance UID string.
|
81
|
+
#
|
82
|
+
def sop_uid
|
83
|
+
return self.generate_uids('3').first
|
84
|
+
end
|
85
|
+
|
86
|
+
# Generates and returns a collection of random SOP Instance UID strings.
|
87
|
+
#
|
88
|
+
# === Parameters
|
89
|
+
#
|
90
|
+
# * <tt>instances</tt> -- Integer. The number of UIDs to generate.
|
91
|
+
#
|
92
|
+
def sop_uids(instances)
|
93
|
+
raise ArgumentError, "Invalid argument 'instances'. Expected Integer, got #{instances.class}." unless instances.is_a?(Integer)
|
94
|
+
return self.generate_uids('3', instances)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Generates and returns a random Study Instance UID string.
|
98
|
+
#
|
99
|
+
def study_uid
|
100
|
+
return self.generate_uids('1').first
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|