Module: DicomS::Support

Included in:
DicomS, DicomS, Sequence, Transfer, Transfer
Defined in:
lib/dicoms/support.rb

Instance Method Summary collapse

Instance Method Details

#assign_dicom_pixels(dicom, pixels) ⇒ Object



337
338
339
340
341
342
# File 'lib/dicoms/support.rb', line 337

def assign_dicom_pixels(dicom, pixels)
  if dicom.compression?
    dicom.delete DICOM::PIXEL_TAG
  end
  dicom.pixels = pixels
end

#cast_metadata(metadata) ⇒ Object



32
33
34
35
36
37
38
39
40
# File 'lib/dicoms/support.rb', line 32

def ()
   = Hash[.to_h.to_a.map { |key, value|
    key = key.to_s.downcase.to_sym
    trans = [key]
    value = value.send(trans) if trans
    [key, value]
  }]
  Settings[]
end

#decode_vector(v) ⇒ Object



159
160
161
# File 'lib/dicoms/support.rb', line 159

def decode_vector(v)
  Vector[*v.split(',').map(&:to_f)]
end

#define_transfer(options, *defaults) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
# File 'lib/dicoms/support.rb', line 185

def define_transfer(options, *defaults)
  strategy, params = Array(options[:transfer])

  unless defaults.first.is_a?(Hash)
    default_strategy = defaults.shift.to_sym
  end
  defautl_strategy ||= :sample
  default_params = defaults.shift || {}
  raise "Invalid number of parametrs" unless defaults.empty?
  Transfer.strategy strategy || default_strategy, default_params.merge((params || {}).to_h)
end

#dicom?(file) ⇒ Boolean

Returns:

  • (Boolean)


70
71
72
73
74
75
76
77
78
79
# File 'lib/dicoms/support.rb', line 70

def dicom?(file)
  ok = false
  if File.file?(file)
    File.open(file, 'rb') do |data|
      data.seek 128, IO::SEEK_SET # skip preamble
      ok = (data.read(4) == 'DICM')
    end
  end
  ok
end

#dicom_bit_depth(dicom) ⇒ Object



243
244
245
246
# File 'lib/dicoms/support.rb', line 243

def dicom_bit_depth(dicom)
  # dicom.send(:bit_depth)
  dicom_element_value dicom, '0028,0100', convert: :to_i
end

#dicom_compression(dicom) ⇒ Object



344
345
346
347
# File 'lib/dicoms/support.rb', line 344

def dicom_compression(dicom)
  ts = DICOM::LIBRARY.uid(dicom.transfer_syntax)
  ts.name if ts.compressed_pixels?
end

#dicom_element_value(dicom, tag, options = {}) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/dicoms/support.rb', line 206

def dicom_element_value(dicom, tag, options = {})
  if dicom.exists?(tag)
    value = dicom[tag].value
    if options[:first]
      if value.is_a?(String)
        value = value.split('\\').first
      elsif value.is_a?(Array)
        value = value.first
      end
    end
    value = value.send(options[:convert]) if options[:convert]
    value
  else
    options[:default]
  end
end

#dicom_name_pattern(name, output_dir) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/dicoms/support.rb', line 167

def dicom_name_pattern(name, output_dir)
  dir = File.dirname(name)
  file = File.basename(name)
  number_pattern = /\d+/
  match = number_pattern.match(file)
  raise "Invalid DICOM file name" unless match
  number = match[0]
  file = file.sub(number_pattern, "%d")
  if match.begin(0) == 0
    # ffmpeg has troubles with filename patterns starting with digits, so we'll add a prefix
    prefix = "d-"
  else
    prefix = nil
  end
  pattern = output_file_name(output_dir, prefix, file)
  [prefix, pattern, number]
end

#dicom_narray(dicom, options = {}) ⇒ Object



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/dicoms/support.rb', line 263

def dicom_narray(dicom, options = {})
  if dicom.compression?
    img = dicom.image
    pixels = dicom.export_pixels(img, dicom.send(:photometry))
    na = NArray.to_na(pixels).reshape!(dicom.num_cols, dicom.num_rows)
    bits = dicom_bit_depth(dicom)
    signed = dicom_signed?(dicom)
    stored_bits = dicom_stored_bits(dicom)
    if stored_bits != Magick::MAGICKCORE_QUANTUM_DEPTH
      use_float = stored_bits < Magick::MAGICKCORE_QUANTUM_DEPTH
      if use_float
        na = na.to_type(NArray::SFLOAT)
        na.mul! 2.0**(stored_bits - Magick::MAGICKCORE_QUANTUM_DEPTH)
        na = na.to_type(NArray::INT)
      else
        na.mul! (1 << (stored_bits - Magick::MAGICKCORE_QUANTUM_DEPTH))
      end
    end
    if remap = options[:remap] || level = options[:level]
      intercept = dicom_rescale_intercept(dicom)
      slope     = dicom_rescale_slope(dicom)
      if intercept != 0 || slope != 1
        na.mul! slope
        na.add! intercept
      end
      if level
        if level.is_a?(Array)
          center, width = level
        else
          center = dicom_window_center(dicom)
          width  = dicom_window_width(dicom)
        end
        if center && width
          low = center - width/2
          high = center + width/2
          na[na < low] = low
          na[na > high] = high
        end
      end

      # Now we limit the output values range.
      # Note that we don't use:
      #   min, max = pixel_value_range(bits, signed)
      # because thats the limits for the stored values, but not for
      # the representation values we're computing here (which are
      # typically signed even if the storage is unsigned)
      # We coud use this, but that would have to be
      #   min, max = pixel_value_range(stored_bits, false)
      #   min = -max
      # but that requires some reviewing.
      # Maybe this shold be parameterized.
      min, max = -65535, 65535
      min_pixel_value = na.min
      if min
        if min_pixel_value < min
          offset = min_pixel_value.abs
          na.add! offset
        end
      end
      max_pixel_value = na.max
      if max
        if max_pixel_value > max
          factor = (max_pixel_value.to_f/max.to_f).ceil
          na.div! factor
        end
      end
    end

    na
  else
    dicom.narray options
  end
end

#dicom_rescale_intercept(dicom) ⇒ Object



235
236
237
# File 'lib/dicoms/support.rb', line 235

def dicom_rescale_intercept(dicom)
  dicom_element_value(dicom, '0028,1052', convert: :to_f, default: 0)
end

#dicom_rescale_slope(dicom) ⇒ Object



239
240
241
# File 'lib/dicoms/support.rb', line 239

def dicom_rescale_slope(dicom)
  dicom_element_value(dicom, '0028,1053', convert: :to_f, default: 1)
end

#dicom_signed?(dicom) ⇒ Boolean

Returns:

  • (Boolean)


248
249
250
251
252
253
254
255
256
# File 'lib/dicoms/support.rb', line 248

def dicom_signed?(dicom)
  # dicom.send(:signed_pixels?)
  case dicom_element_value(dicom, '0028,0103', convert: :to_i)
  when 1
    true
  when 0
    false
  end
end

#dicom_stored_bits(dicom) ⇒ Object



258
259
260
261
# File 'lib/dicoms/support.rb', line 258

def dicom_stored_bits(dicom)
  # dicom.bits_stored.value.to_i
  dicom_element_value dicom, '0028,0101', convert: :to_i
end

#dicom_window_center(dicom) ⇒ Object

WL (window level)



224
225
226
227
# File 'lib/dicoms/support.rb', line 224

def dicom_window_center(dicom)
  # dicom.window_center.value.to_i
  dicom_element_value(dicom, '0028,1050', convert: :to_f, first: true)
end

#dicom_window_width(dicom) ⇒ Object

WW (window width)



230
231
232
233
# File 'lib/dicoms/support.rb', line 230

def dicom_window_width(dicom)
  # dicom.window_center.value.to_i
  dicom_element_value(dicom, '0028,1051', convert: :to_f, first: true)
end

#encode_vector(v) ⇒ Object



155
156
157
# File 'lib/dicoms/support.rb', line 155

def encode_vector(v)
  v.to_a*','
end

#find_dicom_files(dicom_directory) ⇒ Object

Find DICOM files in a directory; Return the file names in an array. DICOM files with a numeric part in the name are returned first, ordered by the numeric value. DICOM files with non-numeric names are returned last ordered by name.



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
# File 'lib/dicoms/support.rb', line 86

def find_dicom_files(dicom_directory)
  # TODO: look recursively inside nested directories
  if File.directory?(dicom_directory)
    dicom_directory = normalized_path(dicom_directory)
    files = Dir.glob(File.join(dicom_directory, '*')).select{|f| dicom?(f)}
  elsif File.file?(dicom_directory) && dicom?(dicom_directory)
    files = [dicom_directory]
  else
    files = []
  end
  non_numeric = []
  numeric_files = []
  files.each do |name|
    base = File.basename(name)
    match = /\d+/.match(base)
    if match
      number = match[0]
      if base =~ /\AI\d\d\d\d\d\d\d\Z/
        # funny scheme found in some DICOMS:
        # the I is followed by the instance number (unpadded), then right
        # padded with zeros, then increased (which affects the last digit)
        # while it coincides with some prior value.
        match = /I(\d\d\d\d)/.match(base)
        number = match[1]
        number = number[0...-1] while number.size > 1 && number[-1] == '0'
        number_zeros = name[-1].to_i
        number << '0'*number_zeros
      end
      numeric_files << [number, name]
    else
      non_numeric << name
    end
  end
  numeric_files.sort_by{ |text, name| text.to_i }.map(&:last) + non_numeric.sort
end

#keeping_pathObject

Code that use images should be wrapped with this.

Reason: if RMagick is used by DICOM to handle images, then the first time it is needed, ‘rmagick’ will be required. This has the effect of placing the path of ImageMagick in front of the PATH. On Windows, ImageMagick includes FFMPeg in its path and we may require a later version than the bundled with IM, so we keep the original path rbefore RMagick alters it. We may be less dependant on the FFMpeg version is we avoid using the start_number option by renumbering the extracted images…



54
55
56
57
58
59
# File 'lib/dicoms/support.rb', line 54

def keeping_path
  path = ENV['PATH']
  yield
ensure
  ENV['PATH'] = path
end

#normalized_path(path) ⇒ Object

Replace ALT_SEPARATOR in pathname (Windows)



62
63
64
65
66
67
68
# File 'lib/dicoms/support.rb', line 62

def normalized_path(path)
  if File::ALT_SEPARATOR
    path.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
  else
    path
  end
end

#output_file_name(dir, prefix, name, ext = '.jpg') ⇒ Object



163
164
165
# File 'lib/dicoms/support.rb', line 163

def output_file_name(dir, prefix, name, ext = '.jpg')
  File.join dir, "#{prefix}#{File.basename(name,'.dcm')}#{ext}"
end

#pixel_value_range(num_bits, signed) ⇒ Object



197
198
199
200
201
202
203
204
# File 'lib/dicoms/support.rb', line 197

def pixel_value_range(num_bits, signed)
  num_values = (1 << num_bits) # 2**num_bits
  if signed
    [-num_values/2, num_values/2-1]
  else
    [0, num_values-1]
  end
end

#single_dicom_metadata(dicom) ⇒ Object



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
# File 'lib/dicoms/support.rb', line 122

def (dicom)
  # 0028,0030 Pixel Spacing:
  dx, dy = dicom.pixel_spacing.value.split('\\').map(&:to_f)
  # 0020,0032 Image Position (Patient):
  x, y, z = dicom.image_position_patient.value.split('\\').map(&:to_f)
  # 0020,0037 Image Orientation (Patient):
  xx, xy, xz, yx, yy, yz = dicom.image_orientation_patient.value.split('\\').map(&:to_f)
  if USE_SLICE_Z
    # according to http://www.vtk.org/Wiki/VTK/FAQ#The_spacing_in_my_DICOM_files_are_wrong
    # this is not reliable
    # 0020,1041 Slice Location:
    slice_z = dicom.slice_location.value.to_f
  else
    slice_z = z
  end

  # 0028,0011 Columns :
  nx = dicom.num_cols # dicom.columns.value.to_i
  # 0028,0010 Rows:
  ny = dicom.num_rows # dicom.rows.value.to_i

  unless dicom.samples_per_pixel.value.to_i == 1
    raise "Invalid DICOM format"
  end
  Settings[
    dx: dx, dy: dy, x: x, y: y, z: z,
    slice_z: slice_z, nx: nx, ny: ny,
    xaxis: encode_vector([xx,xy,xz]),
    yaxis: encode_vector([yx,yy,yz])
    # TODO: + min, max (original values corresponding to 0, 255)
  ]
end