dicoms 1.0.0 → 1.2.0
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.
- checksums.yaml +4 -4
- data/LICENSE.md +0 -2
- data/dicoms.gemspec +2 -1
- data/lib/dicoms.rb +4 -0
- data/lib/dicoms/cli.rb +176 -51
- data/lib/dicoms/explode.rb +421 -0
- data/lib/dicoms/histogram.rb +50 -0
- data/lib/dicoms/info.rb +43 -0
- data/lib/dicoms/progress.rb +22 -7
- data/lib/dicoms/projection.rb +40 -30
- data/lib/dicoms/sequence.rb +16 -11
- data/lib/dicoms/shared_settings.rb +7 -0
- data/lib/dicoms/sigmoid.rb +121 -0
- data/lib/dicoms/stats.rb +2 -2
- data/lib/dicoms/support.rb +48 -2
- data/lib/dicoms/transfer.rb +31 -23
- data/lib/dicoms/version.rb +1 -1
- metadata +34 -2
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'histogram/narray'
|
2
|
+
|
3
|
+
class DicomS
|
4
|
+
def histogram(dicom_directory, options = {})
|
5
|
+
bins, freqs = compute_histogram(dicom_directory, options)
|
6
|
+
print_histogram bins, freqs, options
|
7
|
+
end
|
8
|
+
|
9
|
+
def compute_histogram(dicom_directory, options = {})
|
10
|
+
sequence = Sequence.new(dicom_directory)
|
11
|
+
bin_width = options[:bin_width]
|
12
|
+
|
13
|
+
if sequence.size == 1
|
14
|
+
data = sequence.first.narray
|
15
|
+
else
|
16
|
+
maxx = sequence.metadata.nx
|
17
|
+
maxy = sequence.metadata.ny
|
18
|
+
maxz = sequence.metadata.nz
|
19
|
+
data = NArray.sfloat(maxx, maxy, maxz)
|
20
|
+
sequence.each do |dicom, z, file|
|
21
|
+
data[true, true, z] = sequence.dicom_pixels(dicom)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
bins, freqs = data.histogram(:scott, :bin_boundary => :min, bin_width: bin_width)
|
25
|
+
[bins, freqs]
|
26
|
+
end
|
27
|
+
|
28
|
+
def print_histogram(bins, freqs, options = {})
|
29
|
+
compact = options[:compact]
|
30
|
+
bin_labels = bins.to_a.map { |v| v.round.to_s }
|
31
|
+
label_width = bin_labels.map(&:size).max
|
32
|
+
sep = ": "
|
33
|
+
bar_width = terminal_size.last.to_i - label_width - sep.size
|
34
|
+
div = [1, freqs.max / bar_width.to_f].max
|
35
|
+
compact = true
|
36
|
+
empty = false
|
37
|
+
bin_labels.zip(freqs.to_a).each do |bin, freq|
|
38
|
+
rep = ((freq/div).round)
|
39
|
+
if compact && rep == 0
|
40
|
+
unless empty
|
41
|
+
puts "%#{label_width}s" % ['...']
|
42
|
+
empty = true
|
43
|
+
end
|
44
|
+
next
|
45
|
+
end
|
46
|
+
puts "%#{label_width}s#{sep}%-#{bar_width}s" % [bin, '#'*rep]
|
47
|
+
empty = false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/dicoms/info.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class DicomS
|
4
|
+
def info(dicom_directory, options = {})
|
5
|
+
dicom_files = find_dicom_files(dicom_directory)
|
6
|
+
if dicom_files.empty?
|
7
|
+
raise "ERROR: no se han encontrado archivos DICOM en: \n #{dicom_directory}"
|
8
|
+
end
|
9
|
+
if options[:output]
|
10
|
+
if File.directory?(dicom_directory)
|
11
|
+
output_dir = options[:output]
|
12
|
+
else
|
13
|
+
output_file = options[:output]
|
14
|
+
if File.exists?(output_file)
|
15
|
+
raise "File #{output_file} already exits"
|
16
|
+
end
|
17
|
+
output = File.open(output_file, 'w')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
if output_dir
|
21
|
+
FileUtils.mkdir_p output_dir
|
22
|
+
dicom_files.each do |file|
|
23
|
+
output_file = File.join(output_dir, File.basename(file,'.dcm')+'.txt')
|
24
|
+
File.open(output_file, 'w') do |output|
|
25
|
+
dicom = DICOM::DObject.read(file)
|
26
|
+
print_info dicom, output
|
27
|
+
end
|
28
|
+
end
|
29
|
+
else
|
30
|
+
dicom_files.each do |file|
|
31
|
+
dicom = DICOM::DObject.read(file)
|
32
|
+
print_info dicom, output || STDOUT
|
33
|
+
end
|
34
|
+
output.close if output
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def print_info(dicom, output)
|
39
|
+
$stdout = output
|
40
|
+
dicom.print_all
|
41
|
+
$stdout = STDOUT
|
42
|
+
end
|
43
|
+
end
|
data/lib/dicoms/progress.rb
CHANGED
@@ -6,14 +6,19 @@ class DicomS
|
|
6
6
|
@progress = options[:initial_progress] || 0
|
7
7
|
# TODO: if filename == :console, show progress on the console
|
8
8
|
if filename
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
if description
|
10
|
+
init_options = {
|
11
|
+
override_contents: {
|
12
|
+
process: description,
|
13
|
+
subprocess: options[:subprocess],
|
14
|
+
progress: @progress,
|
15
|
+
error: nil
|
16
|
+
}
|
15
17
|
}
|
16
|
-
|
18
|
+
else
|
19
|
+
init_options = {}
|
20
|
+
end
|
21
|
+
@file = SharedSettings.new(filename, init_options)
|
17
22
|
else
|
18
23
|
@file = nil
|
19
24
|
end
|
@@ -22,6 +27,16 @@ class DicomS
|
|
22
27
|
|
23
28
|
attr_reader :progress
|
24
29
|
|
30
|
+
def persistent?
|
31
|
+
!!@file
|
32
|
+
end
|
33
|
+
|
34
|
+
def error!(code, message)
|
35
|
+
@file.update do |data|
|
36
|
+
data.merge error: code, error_message: message
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
25
40
|
def finished?
|
26
41
|
@progress >= 100
|
27
42
|
end
|
data/lib/dicoms/projection.rb
CHANGED
@@ -69,13 +69,14 @@ class DicomS
|
|
69
69
|
# volume = NArray.sint(maxx, maxy, maxz)
|
70
70
|
volume = NArray.int(maxx, maxy, maxz)
|
71
71
|
end
|
72
|
+
maxv = volume.max
|
72
73
|
keeping_path do
|
73
74
|
sequence.each do |dicom, z, file|
|
74
75
|
slice = sequence.dicom_pixels(dicom, unsigned: true)
|
75
76
|
volume[true, true, z] = slice
|
76
77
|
if center_slice_projection?(options[:axial])
|
77
78
|
minz_contents, maxz_contents = update_min_max_contents(
|
78
|
-
z, slice.max,
|
79
|
+
z, slice.max, maxv, minz_contents, maxz_contents
|
79
80
|
)
|
80
81
|
end
|
81
82
|
progress.update_subprocess z
|
@@ -85,7 +86,7 @@ class DicomS
|
|
85
86
|
if center_slice_projection?(options[:coronal])
|
86
87
|
(0...maxy).each do |y|
|
87
88
|
miny_contents, maxy_contents = update_min_max_contents(
|
88
|
-
y, volume[true, y, true].max,
|
89
|
+
y, volume[true, y, true].max, maxv, miny_contents, maxy_contents
|
89
90
|
)
|
90
91
|
end
|
91
92
|
end
|
@@ -93,7 +94,7 @@ class DicomS
|
|
93
94
|
if center_slice_projection?(options[:sagittal])
|
94
95
|
(0...maxz).each do |z|
|
95
96
|
minz_contents, maxz_contents = update_min_max_contents(
|
96
|
-
z, volume[true, true, z].max,
|
97
|
+
z, volume[true, true, z].max, maxv, minz_contents, maxz_contents
|
97
98
|
)
|
98
99
|
end
|
99
100
|
end
|
@@ -129,7 +130,7 @@ class DicomS
|
|
129
130
|
elsif middle_slice_projection?(options[:coronal])
|
130
131
|
coronal_ys = [[maxy/2, 'm']]
|
131
132
|
elsif full_projection?(options[:coronal])
|
132
|
-
coronal_ys = (0...
|
133
|
+
coronal_ys = (0...maxy)
|
133
134
|
else
|
134
135
|
coronal_ys = []
|
135
136
|
end
|
@@ -275,8 +276,7 @@ class DicomS
|
|
275
276
|
v.max(axis)
|
276
277
|
end
|
277
278
|
|
278
|
-
def accumulated_attenuation_projection(float_v, axis, max_output_level, max=500)
|
279
|
-
k = 0.02
|
279
|
+
def accumulated_attenuation_projection(float_v, axis, max_output_level, max=500, k = 0.02)
|
280
280
|
if ADJUST_AAP_FOR_WIDTH
|
281
281
|
k *= 500.0/max
|
282
282
|
end
|
@@ -284,8 +284,10 @@ class DicomS
|
|
284
284
|
v.mul! -k
|
285
285
|
v = NMath.exp(v)
|
286
286
|
# Invert result (from attenuation to transmission)
|
287
|
-
|
288
|
-
|
287
|
+
if max_output_level
|
288
|
+
v.mul! -max_output_level
|
289
|
+
v.add! max_output_level
|
290
|
+
end
|
289
291
|
v
|
290
292
|
end
|
291
293
|
|
@@ -322,8 +324,6 @@ class DicomS
|
|
322
324
|
end
|
323
325
|
|
324
326
|
def projection_scaling(data, options = {})
|
325
|
-
sx = sy = sz = 1
|
326
|
-
|
327
327
|
nx = data.nx
|
328
328
|
ny = data.ny
|
329
329
|
nz = data.nz
|
@@ -331,18 +331,37 @@ class DicomS
|
|
331
331
|
dy = data.dy
|
332
332
|
dz = data.dz
|
333
333
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
334
|
+
sx = sy = sz = 1
|
335
|
+
scaled_nx = nx
|
336
|
+
scaled_ny = ny
|
337
|
+
scaled_nz = nz
|
338
|
+
|
339
|
+
adjust = ->(ref) {
|
338
340
|
sx = dx/ref
|
339
341
|
sy = dy/ref
|
340
342
|
sz = dz/ref
|
341
|
-
|
343
|
+
scaled_nx = (nx*sx).round
|
344
|
+
scaled_ny = (ny*sy).round
|
345
|
+
scaled_nz = (nz*sz).round
|
346
|
+
}
|
347
|
+
|
348
|
+
# need to cope with different scales in different axes.
|
349
|
+
# will always produce shrink factors (<1)
|
350
|
+
adjust.call [dx, dy, dz].max.to_f
|
342
351
|
|
343
|
-
|
344
|
-
|
345
|
-
|
352
|
+
# Now we may have scaled down too much; we'll avoid going below
|
353
|
+
# requested minimum sizes for axes and/or image dimensions.
|
354
|
+
# Axis X is the columns of axial an coronal views
|
355
|
+
# Axis Y is the rows of axial and the columns of sagittal
|
356
|
+
# Axis Z is the rows of coronal and sagittal views
|
357
|
+
min_nx = [scaled_nx, options.min_x_pixels, options[:mincols]].compact.max
|
358
|
+
adjust[dx*nx.to_f/min_nx] if scaled_nx < min_nx
|
359
|
+
|
360
|
+
min_ny = [scaled_ny, options.min_y_pixels, options[:mincols], options[:minrows]].compact.max
|
361
|
+
adjust[dy*ny.to_f/min_ny] if scaled_ny < min_ny
|
362
|
+
|
363
|
+
min_nz = [scaled_nz, options.min_z_pixels, options[:minrows]].compact.max
|
364
|
+
adjust[dz*nz.to_f/min_nz] if scaled_nz > min_nz
|
346
365
|
|
347
366
|
# further shrinking may be needed to avoid any projection
|
348
367
|
# to be larger thant the maximum image size
|
@@ -350,22 +369,13 @@ class DicomS
|
|
350
369
|
# Axis Y is the rows of axial and the columns of sagittal
|
351
370
|
# Axis Z is the rows of coronal and sagittal views
|
352
371
|
max_nx = [scaled_nx, options.max_x_pixels, options[:maxcols]].compact.min
|
353
|
-
if scaled_nx > max_nx
|
354
|
-
scaled_nx = max_nx
|
355
|
-
sx = scaled_nx/nx.to_f
|
356
|
-
end
|
372
|
+
adjust[dx*nx.to_f/max_nx] if scaled_nx > max_nx
|
357
373
|
|
358
374
|
max_ny = [scaled_ny, options.max_y_pixels, options[:maxcols], options[:maxrows]].compact.min
|
359
|
-
if scaled_ny > max_ny
|
360
|
-
scaled_ny = max_ny
|
361
|
-
sy = scaled_ny/ny.to_f
|
362
|
-
end
|
375
|
+
adjust[dy*ny.to_f/max_ny] if scaled_ny > max_ny
|
363
376
|
|
364
377
|
max_nz = [scaled_nz, options.max_z_pixels, options[:maxrows]].compact.min
|
365
|
-
if scaled_nz > max_nz
|
366
|
-
scaled_nz = max_nz
|
367
|
-
sz = scaled_nz/nz.to_f
|
368
|
-
end
|
378
|
+
adjust[dz*nz.to_f/max_nz] if scaled_nz > max_nz
|
369
379
|
|
370
380
|
{
|
371
381
|
scale_x: sx, scale_y: sy, scale_z: sz,
|
data/lib/dicoms/sequence.rb
CHANGED
@@ -79,6 +79,9 @@ class DicomS
|
|
79
79
|
@strategy = options[:transfer]
|
80
80
|
@metadata = Settings[version: 'DSPACK1']
|
81
81
|
|
82
|
+
if @files.empty?
|
83
|
+
raise InvaliddDICOM, 'No DICOM files found'
|
84
|
+
end
|
82
85
|
# TODO: reuse existing metadata in options (via settings)
|
83
86
|
|
84
87
|
if options[:reorder]
|
@@ -131,7 +134,7 @@ class DicomS
|
|
131
134
|
dicom = DICOM::DObject.read(@files[i])
|
132
135
|
sop_class = dicom['0002,0002'].value
|
133
136
|
unless sop_class == '1.2.840.10008.5.1.4.1.1.2'
|
134
|
-
raise "Unsopported SOP Class #{sop_class}"
|
137
|
+
raise UnsupportedDICOM, "Unsopported SOP Class #{sop_class}"
|
135
138
|
end
|
136
139
|
# TODO: require known SOP Class:
|
137
140
|
# (in tag 0002,0002, Media Storage SOP Class UID)
|
@@ -244,7 +247,7 @@ class DicomS
|
|
244
247
|
end
|
245
248
|
if bits
|
246
249
|
if bits != dicom_bit_depth(dicom) || signed != dicom_signed?(dicom)
|
247
|
-
raise "Inconsistent slices"
|
250
|
+
raise InvalidDICOM, "Inconsistent slices"
|
248
251
|
end
|
249
252
|
else
|
250
253
|
bits = dicom_bit_depth(dicom)
|
@@ -252,7 +255,7 @@ class DicomS
|
|
252
255
|
end
|
253
256
|
if slope
|
254
257
|
if slope != dicom_rescale_slope(dicom) || intercept != dicom_rescale_intercept(dicom)
|
255
|
-
raise "Inconsitent slices"
|
258
|
+
raise InvalidDICOM, "Inconsitent slices"
|
256
259
|
end
|
257
260
|
else
|
258
261
|
slope = dicom_rescale_slope(dicom)
|
@@ -322,8 +325,6 @@ class DicomS
|
|
322
325
|
|
323
326
|
total_n = size
|
324
327
|
|
325
|
-
n = last_i - first_i
|
326
|
-
|
327
328
|
xaxis = decode_vector(first_md.xaxis)
|
328
329
|
yaxis = decode_vector(first_md.yaxis)
|
329
330
|
# assert xaxis == decode_vector(last_md.xaxis)
|
@@ -337,12 +338,16 @@ class DicomS
|
|
337
338
|
@metadata.merge! Settings[first_md]
|
338
339
|
@metadata.zaxis = encode_vector zaxis
|
339
340
|
@metadata.nz = total_n
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
341
|
+
# Position coordinates are assumed at the voxel's center
|
342
|
+
# note that we divide the coordinate span z_last - z_first
|
343
|
+
# by the number of slices (voxels) minus one
|
344
|
+
@metadata.dz = (last_md.slice_z - first_md.slice_z).abs/(last_i - first_i)
|
345
|
+
|
346
|
+
tolerance = 0.05
|
347
|
+
if (xaxis.map(&:abs) - Vector[1,0,0]).magnitude > tolerance ||
|
348
|
+
(yaxis.map(&:abs) - Vector[0,1,0]).magnitude > tolerance ||
|
349
|
+
(zaxis.map(&:abs) - Vector[0,0,1]).magnitude > tolerance
|
350
|
+
raise UnsupportedDICOM, "Unsupported orientation"
|
346
351
|
end
|
347
352
|
@metadata.reverse_x = (xaxis[0] < 0) ? 1 : 0
|
348
353
|
@metadata.reverse_y = (yaxis[1] < 0) ? 1 : 0
|
@@ -24,6 +24,13 @@ class DicomS
|
|
24
24
|
end
|
25
25
|
contents = options[:replace_contents]
|
26
26
|
write contents if contents
|
27
|
+
contents = options[:override_contents]
|
28
|
+
if contents
|
29
|
+
update do |data|
|
30
|
+
data ||= {}
|
31
|
+
data.merge contents
|
32
|
+
end
|
33
|
+
end
|
27
34
|
end
|
28
35
|
|
29
36
|
# Read a shared file and obtain a Settings object
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'solver'
|
2
|
+
require 'narray'
|
3
|
+
|
4
|
+
class DicomS
|
5
|
+
|
6
|
+
# Compute sigmoid function s(x) = 1 - exp(-((x - x0)**gamma)/k) for x >= x0;
|
7
|
+
# with s(x) = 0 for x < x0).
|
8
|
+
#
|
9
|
+
# 0 <= s(x) < 1 for all x
|
10
|
+
#
|
11
|
+
# Defined by two parameters:
|
12
|
+
#
|
13
|
+
# * w width of the sigmoid
|
14
|
+
# * xc center of the sigmoid (s(xc) = 1/2)
|
15
|
+
#
|
16
|
+
# The interpretation of w is established by a constant 0 < k0 << 1
|
17
|
+
# where y1 = s(x1) = k0 and y2 = s(x2) = 1 - k0 and
|
18
|
+
# x1 = xc - w/2 and x2 = xc + w/2
|
19
|
+
#
|
20
|
+
# The gamma factor should be fixed a priori, e.g. gamma = 4
|
21
|
+
#
|
22
|
+
# k0 = 0.01
|
23
|
+
# w = 3.0
|
24
|
+
# xc = 7.0
|
25
|
+
# sigmod = Sigmoid.new(k0, w, xc)
|
26
|
+
# 0.5 == sigmoid[xc]
|
27
|
+
# k0 == sigmod[xc - w/2]
|
28
|
+
# 1 - k0 == sigmod[xc + w/2]
|
29
|
+
class Sigmoid
|
30
|
+
|
31
|
+
def initialize(options = {})
|
32
|
+
k0 = options[:k0] || 0.05
|
33
|
+
w = options[:width]
|
34
|
+
xc = options[:center]
|
35
|
+
@gamma = options[:gamma] || 4.0
|
36
|
+
tolerance = options[:tolerance] || Flt::Tolerance(0.01, :relative)
|
37
|
+
|
38
|
+
# Compute the parameters x0 and gamma which define the sigmoid
|
39
|
+
# given k0, w and xc
|
40
|
+
|
41
|
+
yc = 0.5
|
42
|
+
x1 = xc - w/2
|
43
|
+
x2 = xc + w/2
|
44
|
+
y1 = k0
|
45
|
+
y2 = 1 - k0
|
46
|
+
@gamma = 4.0
|
47
|
+
|
48
|
+
# We have two equations to solve for x0, gamma:
|
49
|
+
# * yc == 1 - Math.exp(-((xc - x0)**gamma)/k)
|
50
|
+
# * y1 == 1 - Math.exp(-((x1 - x0)**gamma)/k)
|
51
|
+
# Equivalently we could substitute the latter for
|
52
|
+
# * y2 == 1 - Math.exp(-((x2 - x0)**gamma)/k)
|
53
|
+
#
|
54
|
+
# So, we use the first equation to substitute in the second
|
55
|
+
# one of these:
|
56
|
+
#
|
57
|
+
# * x0 = xc - (-k*Math.log(1 - yc))**(1/gamma)
|
58
|
+
# * k = - (xc - x0)**gamma / Math.log(1 - yc)
|
59
|
+
#
|
60
|
+
# * gamma = Math.log(-k*Math.log(1 - yc))/Math.log(xc - x0)
|
61
|
+
#
|
62
|
+
# So we could solve either this for k:
|
63
|
+
#
|
64
|
+
# * y1 == 1 - Math.exp(-((x1 - xc + (-k*Math.log(1 - yc))**(1/gamma))**gamma)/k)
|
65
|
+
#
|
66
|
+
# or this for x0
|
67
|
+
#
|
68
|
+
# * y1 == 1 - Math.exp(-((x1 - x0)**gamma)/(- (xc - x0)**gamma / Math.log(1 - yc)))
|
69
|
+
algorithm = Flt::Solver::RFSecantSolver
|
70
|
+
|
71
|
+
if true
|
72
|
+
g = @gamma
|
73
|
+
eq = ->(k) {
|
74
|
+
y1 - 1 + exp(-((x1 - xc + (-k*log(1 - yc))**(1/g))**g)/k) }
|
75
|
+
solver = algorithm.new(Float.context, tolerance, &eq)
|
76
|
+
@k = solver.root(1.0, 100.0)
|
77
|
+
@x0 = xc - (-@k*Math.log(1 - yc))**(1/@gamma)
|
78
|
+
else
|
79
|
+
g = @gamma
|
80
|
+
eq = ->(x0) { y1 - 1 + exp(-((x1 - x0)**g)/(- (xc - x0)**g / log(1 - yc))) }
|
81
|
+
solver = algorithm.new(Float.context, tolerance, &eq)
|
82
|
+
@x0 = solver.root(0.0, 100.0)
|
83
|
+
@k = - (xc - @x0)**@gamma / Math.log(1 - yc)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def [](x)
|
88
|
+
if x.is_a? NArray
|
89
|
+
narray_sigmoid x
|
90
|
+
else
|
91
|
+
if x <= @x0
|
92
|
+
0.0
|
93
|
+
else
|
94
|
+
1.0 - Math.exp(-((x - @x0)**@gamma)/@k)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
attr_reader :gamma, :k, :x0
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def narray_sigmoid(x)
|
104
|
+
x[x <= @x0] = 0
|
105
|
+
ne0 = x.ne(0)
|
106
|
+
y = x[ne0]
|
107
|
+
y.sbt! @x0
|
108
|
+
y = power(y, @gamma)
|
109
|
+
y.div! -@k
|
110
|
+
y = NMath.exp(y)
|
111
|
+
y.mul! -1
|
112
|
+
y.add! 1
|
113
|
+
x[ne0] = y
|
114
|
+
x
|
115
|
+
end
|
116
|
+
|
117
|
+
def power(x, y)
|
118
|
+
NMath.exp(y*NMath.log(x))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|