rtkit 0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|