From 267f57cb99f473d0d39fc150048f1a173343e98e Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sat, 17 Mar 2012 03:05:51 -0500 Subject: [PATCH 01/10] Beginning of work on Image types. --- examples/image.jl | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/examples/image.jl b/examples/image.jl index 6e05704e48348..52bae1097ba09 100644 --- a/examples/image.jl +++ b/examples/image.jl @@ -1,3 +1,58 @@ +abstract ImageCoordinate +abstract Space <: ImageCoordinate +abstract Time <: ImageCoordinate +abstract Channel <: ImageCoordinate + +abstract Image +# Defines an image type with all data held in memory +type ImageArray{T<:Number} <: Image + data::Array{T} + coordinate_types::Vector{ImageCoordinate} + coordinate_units::Vector{Any} # vector of strings, "microns" or I"\mu m" + coordinate_names::Vector{Any} # vector of strings, "X" or "Y" + coordinate_ranges::Vector{Range1} + space_directions::Matrix{Float64} + valid::Array{Any,1} # can be used to store bad frame/pixel data + metadata::CompositeKind # arbitrary metadata, like acquisition time, etc. + + ImageArray{T}() = new() +end +function ImageArray{T<:Number}(dat::Array{T}) + ret = ImageArray{T}() + ret.data = dat + n_dims = ndims(dat) + ret.coordinate_types = cell(n_dims) + ret.coordinate_types[1:n_dims] = Space + ret.coordinate_units = cell(n_dims) + ret.coordinate_units[1:n_dims] = "" + ret.coordinate_names = cell(n_dims) + ret.coordinate_names[1:n_dims] = "" + ret.space_directions = eye(n_dims) + ret.valid = cell(n_dims) + onec = cell(n_dims) + onec[1:n_dims] = 1 + for idim = 1:n_dims + sz = copy(onec) + sz[idim] = size(dat,idim) + ret.valid[idim] = trues(tuple(sz...)) + end + return ret +end +function ndims(img::ImageArray) + return length(img.coordinate_types) +end +function set_orthogonal_spacing(img::Image,s::Vector) + n_dims = ndims(img) + if n_dims != length(s) + error("Dimensions do not match") + end + for idim = 1:n_dims + img.space_directions[idim,idim] = s[idim] + end + return img +end + + function lut(pal::Vector, a) out = similar(a, eltype(pal)) n = numel(pal) From e423ca1ce7ac4c5be4a8fa9ffccc7d69d1173bb8 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sun, 18 Mar 2012 14:43:21 -0500 Subject: [PATCH 02/10] Raise issue and possible fix re emacs indentation. This does not fix a bug, but it highlights where I think it occurs and suggests a possible fix for it. I'm not an elisp wizard, so I decided that discretion was the better part of valor. --- contrib/julia-mode.el | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contrib/julia-mode.el b/contrib/julia-mode.el index d352a697217cf..83feb696d613b 100644 --- a/contrib/julia-mode.el +++ b/contrib/julia-mode.el @@ -97,6 +97,11 @@ ; TODO: skip keywords inside strings +; fixme? is the line-beginning-position necessary? +; Currently you are fine with this kind of comment: +; # Here's a comment with the word "for" in it +; but not with this: +; x = 5 # Here's another comment with the word "for" in it (defun in-comment () (equal (char-after (+ (line-beginning-position) (current-indentation))) ?#)) From 4ff6e8d9ee01b284efdb1b648fab90c29b841dc3 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 19 Mar 2012 02:36:14 -0500 Subject: [PATCH 03/10] Initial checkin of ImageArray class. This isn't debugged yet because I'm getting a segfault. Committing now so I can merge with master and make sure I'm updated to the latest Julia. --- extras/image.jl | 206 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 162 insertions(+), 44 deletions(-) diff --git a/extras/image.jl b/extras/image.jl index ea642cad4a0ef..e6423ab9583a6 100644 --- a/extras/image.jl +++ b/extras/image.jl @@ -1,58 +1,176 @@ -abstract ImageCoordinate -abstract Space <: ImageCoordinate -abstract Time <: ImageCoordinate -abstract Channel <: ImageCoordinate - +# The super-type of all images abstract Image -# Defines an image type with all data held in memory -type ImageArray{T<:Number} <: Image - data::Array{T} - coordinate_types::Vector{ImageCoordinate} - coordinate_units::Vector{Any} # vector of strings, "microns" or I"\mu m" - coordinate_names::Vector{Any} # vector of strings, "X" or "Y" - coordinate_ranges::Vector{Range1} - space_directions::Matrix{Float64} - valid::Array{Any,1} # can be used to store bad frame/pixel data - metadata::CompositeKind # arbitrary metadata, like acquisition time, etc. - - ImageArray{T}() = new() -end -function ImageArray{T<:Number}(dat::Array{T}) - ret = ImageArray{T}() - ret.data = dat - n_dims = ndims(dat) - ret.coordinate_types = cell(n_dims) - ret.coordinate_types[1:n_dims] = Space - ret.coordinate_units = cell(n_dims) - ret.coordinate_units[1:n_dims] = "" - ret.coordinate_names = cell(n_dims) - ret.coordinate_names[1:n_dims] = "" - ret.space_directions = eye(n_dims) - ret.valid = cell(n_dims) - onec = cell(n_dims) - onec[1:n_dims] = 1 - for idim = 1:n_dims - sz = copy(onec) - sz[idim] = size(dat,idim) - ret.valid[idim] = trues(tuple(sz...)) + +# General principles: + +# Image types implement fields needed to specify an image data array +# A, and all fields critical to the interpretation of this array. In +# addition, a "metadata" field is present so that users can store +# other information as desired. + +# The dimensions of the data array fall into 3 general categories: +# space dimensions (e.g., x and y), time (if the data array +# represents a sequence of images over time), and channel dimension +# (e.g., color index). It's important to know the storage order of +# the data array. This is signaled by a storage order string, which +# introduces a one-character name for each array dimension. For +# example, one could create an image out of a data array A[y,x,c] as +# im = ImageArray(A,"yxc") +# Images can be manipulated in terms of these coordinate names, +# e.g., +# imsnip = ref(im,'x',4) +# would yield an image called imsnip that is a slice of im along the +# x-coordinate. This would work no matter how im is stored, but for +# this example is equivalent to +# imsnip.data = im.data[:,4,:]. +# ref is used instead of slicedim because the following also works: +# imsnip = ref(im,'x',3:23,'y',100:200) +# +# There are two reserved choices for the array dimension names: 't' +# (for time) and 'c' (for channel). All other choices are assumed to +# be spatial. The comparison is case-sensitive, so 'T' and 'C' can +# be used for spatial axes. Thus, "yxc" might be used for an RGB +# image with color data in the third array dimension, and "xyzt" for +# a 3d grayscale image over time. +# +# Other than the usage of 't' and 'c', the choices of dimension +# names is arbitrary. One suggestion is to choose a name that +# indicates the direction of increasing array index. For example, +# choosing "ur" (for "upper-right") instead of "yx" might more +# clearly signal that the lower-left corner (as displayed on the +# screen) should be A[1,1], and the upper-right corner is +# A[sz1,sz2]. In MRI, "RAS" might indicate a standard right-handed +# coordinate system ('R' = rightward-increasing, 'A' = +# anterior-increasing, 'S' = superior-increasing). Note that "ASR" +# would imply the same coordinate system but that the data are +# stored in [anterior/posterior, superior/inferior, right/left] +# order. +# +# The remaining "principal" fields are those that are minimally needed +# to interpret the data array: +# The spatial geometry is specified by fields that document how +# indices for the array's spatial dimensions correspond to +# physical space (any linear relationship is supported, via a +# transform matrix); +# The timing information is supplied by a lookup vector specifying +# the time of each temporal slice; +# The channel data is specified in terms of a colorspace string +# ("sRGB"), or a list of strings specifying the meaning of each +# channel index (e.g., ["GFP","tdTomato"] for a 2-color +# fluorescence image) + +# More detail on spatial specifications: +# arrayi stands for "array index", i.e., the (integer) indices into +# the data array +# physc means "physical coordinate", i.e., units in actual space (e.g., +# position in millimeters) +# arrayi2physc is a n-by-(n+1) transform matrix T; p = T*[a,1] would +# take a column vector a containing the index coordinates of a +# single element and convert it into a column vector of physical +# coordinates. In other words, the separation between adjacent +# "pixels" along the jth array dimension corresponds to a +# physical-space displacement of T*[ej,0], where ej is the unit +# vector along the jth dimension. The right-hand column of T can be +# used to encode translations, so that the (ficticious) array item +# A[0,...,0] corresponds to a physical-space origin of coordinates +# at position T[:,end]. There are utility functions that make +# it easy to specify T in common cases, e.g., +# set_pixel_spacing(im,[0.15 0.15 3]) +# would create a transform matrix for a confocal microscopy stack +# with 0.15 microns between pixels in the x- and y- dimensions, and +# 3 microns between slices along the z-dimension, with the result +# T = [0.15 0 0 0; +# 0 0.15 0 0; +# 0 0 3 0] +# + +# A final important task is representing valid pixels, since real-world +# imaging devices/situations can result in a subset of the data +# being untrustworthy. In cases where the underlying data type The valid field can be set to true (all +# pixels are trustworthy), a boolean matrix of the size of the data +# array (marking trustworthy pixels individually), or as a container +# of arrays whose product would be equal to the full-size valid +# pixels array. For example, +# valid = [good_pixels_per_frame,good_frames] +# where good_pixels_per_frame is a boolean of size [sizex sizey] and +# good_frames is a boolean of size [1 1 n_frames]. + + +# A few utility functions: +# Make sure the storage order string doesn't duplicate any names +function assert_chars_unique(s::ASCIIString) + ss = sort(b"$s") + for i = 2:length(ss) + if ss[i] == ss[i-1] + error("Array dimension names cannot repeat") + end end - return ret end -function ndims(img::ImageArray) - return length(img.coordinate_types) + +# An image type with all data held in memory +type ImageArray{DataType<:Number} <: Image + data::Array{DataType} # the raw data + arrayi_order::ASCIIString # storage order of data array, e.g. "yxc" + data_size::Vector{Int} # full size of the original array + arrayi_range::Vector # vector of ranges (when snipping out a block) + arrayi2physc::Matrix{Float64} # transform matrix + physc_unit::Vector # vector of strings, e.g., "microns" + physc_name::Vector # vector of strings, like "X" or "horizontal" + arrayti2physt::Vector{Float64}# time coordinate lookup table + t_unit::String # time coordinate unit string + color_space # string or vector of strings, e.g. "sRGB" + valid # which pixels can be trusted? + metadata # arbitrary metadata, like acquisition date&time, etc. +end +# Empty constructor +ImageArray{DataType<:Number}() = ImageArray{DataType}([],"",[],[],[],[],[],[],"","",false,[]) +# Construct from a data array +function ImageArray{DataType<:Number}(data::Array{DataType},arrayi_order::ASCIIString) + sz = size(data) + n_dims = length(sz) + if strlen(arrayi_order) != n_dims + error("storage order string must have a length equal to the number of dimensions in the array") + end + # Enforce uniqueness of each array coordinate name + assert_chars_unique(arrayi_order) + # Count # of spatial dimensions + matcht = match(r"t",arrayi_order) + matchc = match(r"c",arrayi_order) + n_spatial_dims = n_dims - !is(matcht,nothing) - !is(matchc,nothing) + println("n_spatial_dims = $n_spatial_dims") + # Set up defaults for other fields + arrayi_range = cell(n_dims) + for idim = 1:n_dims + arrayi_range[idim] = 1:sz[idim] + end + physc_unit = cell(n_spatial_dims) + physc_name = cell(n_spatial_dims) + physc_unit[1:n_spatial_dims] = "" + physc_name[1:n_spatial_dims] = "" + T = [eye(n_spatial_dims) zeros(n_spatial_dims)] + if !is(matcht,nothing) + tindex = matcht.offset + arrayti2physt = linspace(1,sz[tindex],sz[tindex]) + else + arrayti2physt = [] + end + ImageArray{DataType}(data,arrayi_order,sz,arrayi_range,T,physc_unit,physc_name,arrayti2physt,"","",true,[]) end -function set_orthogonal_spacing(img::Image,s::Vector) - n_dims = ndims(img) - if n_dims != length(s) + +function set_pixel_spacing(img::Image,dx::Vector) + n_spatial_dims = size(img.arrayi2physc,1) + if n_spatial_dims != length(dx) error("Dimensions do not match") end - for idim = 1:n_dims - img.space_directions[idim,idim] = s[idim] + for idim = 1:n_spatial_dims + img.arrayi2physc[idim,idim] = dx[idim] end return img end + + function lut(pal::Vector, a) out = similar(a, eltype(pal)) n = numel(pal) From 4dcea237bb2d9fac961d91b504e8288d841ec55a Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 19 Mar 2012 07:17:07 -0500 Subject: [PATCH 04/10] Gets the ImageArray type up and working. - This works around some segfaults, probably by being more type-correct. - It adds support for a few methods dealing with subscripting. --- extras/image.jl | 94 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 71 insertions(+), 23 deletions(-) diff --git a/extras/image.jl b/extras/image.jl index e6423ab9583a6..5093caa2fe17f 100644 --- a/extras/image.jl +++ b/extras/image.jl @@ -11,35 +11,35 @@ abstract Image # The dimensions of the data array fall into 3 general categories: # space dimensions (e.g., x and y), time (if the data array # represents a sequence of images over time), and channel dimension -# (e.g., color index). It's important to know the storage order of +# (e.g., color channel index). It's important to know the storage order of # the data array. This is signaled by a storage order string, which # introduces a one-character name for each array dimension. For # example, one could create an image out of a data array A[y,x,c] as -# im = ImageArray(A,"yxc") +# img = ImageArray(A,"yxc") # Images can be manipulated in terms of these coordinate names, # e.g., -# imsnip = ref(im,'x',4) +# imsnip = ref(img,'x',4) # would yield an image called imsnip that is a slice of im along the -# x-coordinate. This would work no matter how im is stored, but for -# this example is equivalent to -# imsnip.data = im.data[:,4,:]. +# x-coordinate. This would work no matter how img is stored; for +# this example, it is equivalent to +# imsnip.data = img.data[:,4,:]. # ref is used instead of slicedim because the following also works: -# imsnip = ref(im,'x',3:23,'y',100:200) +# imsnip = ref(img,'x',3:23,'y',100:200) # # There are two reserved choices for the array dimension names: 't' # (for time) and 'c' (for channel). All other choices are assumed to # be spatial. The comparison is case-sensitive, so 'T' and 'C' can # be used for spatial axes. Thus, "yxc" might be used for an RGB # image with color data in the third array dimension, and "xyzt" for -# a 3d grayscale image over time. +# 3d grayscale over time. # # Other than the usage of 't' and 'c', the choices of dimension # names is arbitrary. One suggestion is to choose a name that # indicates the direction of increasing array index. For example, -# choosing "ur" (for "upper-right") instead of "yx" might more +# choosing "br" (for "bottom-right") instead of "yx" might more # clearly signal that the lower-left corner (as displayed on the -# screen) should be A[1,1], and the upper-right corner is -# A[sz1,sz2]. In MRI, "RAS" might indicate a standard right-handed +# screen) should be A[sz1,1], and the upper-right corner is +# A[1,sz2]. In MRI, "RAS" might indicate a standard right-handed # coordinate system ('R' = rightward-increasing, 'A' = # anterior-increasing, 'S' = superior-increasing). Note that "ASR" # would imply the same coordinate system but that the data are @@ -106,13 +106,13 @@ function assert_chars_unique(s::ASCIIString) end end end - + # An image type with all data held in memory type ImageArray{DataType<:Number} <: Image data::Array{DataType} # the raw data arrayi_order::ASCIIString # storage order of data array, e.g. "yxc" data_size::Vector{Int} # full size of the original array - arrayi_range::Vector # vector of ranges (when snipping out a block) + arrayi_range::Vector{Range1{Int}} # vector of ranges (snipping out blocks) arrayi2physc::Matrix{Float64} # transform matrix physc_unit::Vector # vector of strings, e.g., "microns" physc_name::Vector # vector of strings, like "X" or "horizontal" @@ -122,11 +122,12 @@ type ImageArray{DataType<:Number} <: Image valid # which pixels can be trusted? metadata # arbitrary metadata, like acquisition date&time, etc. end -# Empty constructor -ImageArray{DataType<:Number}() = ImageArray{DataType}([],"",[],[],[],[],[],[],"","",false,[]) +# Empty constructor (doesn't work right now for some reason) +ImageArray{DataType<:Number}() = ImageArray{DataType}(Array(DataType,0),"",Array(Int,0),Array(Range1,0),zeros(0,0),Array(ASCIIString,0),Array(ASCIIString,0),zeros(0),"","",false,[]) # Construct from a data array function ImageArray{DataType<:Number}(data::Array{DataType},arrayi_order::ASCIIString) sz = size(data) + szv = vcat(sz...) n_dims = length(sz) if strlen(arrayi_order) != n_dims error("storage order string must have a length equal to the number of dimensions in the array") @@ -137,24 +138,35 @@ function ImageArray{DataType<:Number}(data::Array{DataType},arrayi_order::ASCIIS matcht = match(r"t",arrayi_order) matchc = match(r"c",arrayi_order) n_spatial_dims = n_dims - !is(matcht,nothing) - !is(matchc,nothing) - println("n_spatial_dims = $n_spatial_dims") # Set up defaults for other fields - arrayi_range = cell(n_dims) + arrayi_range = Array(Range1{Int},n_dims) for idim = 1:n_dims arrayi_range[idim] = 1:sz[idim] end - physc_unit = cell(n_spatial_dims) - physc_name = cell(n_spatial_dims) + physc_unit = Array(ASCIIString,n_spatial_dims) + physc_name = Array(ASCIIString,n_spatial_dims) physc_unit[1:n_spatial_dims] = "" physc_name[1:n_spatial_dims] = "" T = [eye(n_spatial_dims) zeros(n_spatial_dims)] if !is(matcht,nothing) tindex = matcht.offset - arrayti2physt = linspace(1,sz[tindex],sz[tindex]) + arrayti2physt = linspace(1.0,sz[tindex],sz[tindex]) + t_unit = "" else - arrayti2physt = [] + arrayti2physt = zeros(0) + t_unit = "" + end + color_space = "" + if !is(matchc,nothing) + if size(data,matchc.offset) == 1 + color_space = "gray" + elseif size(data,matchc.offset) == 3 + color_space = "sRGB" + elseif szv[matchc.offset] == 4 + color_space = "CMYK" + end end - ImageArray{DataType}(data,arrayi_order,sz,arrayi_range,T,physc_unit,physc_name,arrayti2physt,"","",true,[]) + ImageArray{DataType}(data,arrayi_order,szv,arrayi_range,T,physc_unit,physc_name,arrayti2physt,t_unit,color_space,true,[]) end function set_pixel_spacing(img::Image,dx::Vector) @@ -165,11 +177,47 @@ function set_pixel_spacing(img::Image,dx::Vector) for idim = 1:n_spatial_dims img.arrayi2physc[idim,idim] = dx[idim] end - return img end +function get_pixel_spacing(img::Image) + n_spatial_dims = size(img.arrayi2physc,1) + dx = zeros(n_spatial_dims) + for idim = 1:n_spatial_dims + dx[idim] = img.arrayi2physc[idim,idim] + end + return dx +end + +function ref{DataType}(img::ImageArray{DataType},cv...) + if length(cv) % 2 != 0 + error("Coordinate/value must come in pairs") + end + imgret = copy(img) + # Prepare the coordinates for snipping + cc = cell(ndims(img.data)) + for idim = 1:ndims(img.data) + cc[idim] = 1:size(img.data,idim) + end + for icv = 1:2:length(cv) + idim = strchr(img.arrayi_order,cv[icv]) + cc[idim] = cv[icv+1] + end + # Do the snip + imgret.data = img.data[cc...] + imgret.arrayi_range = cc + return imgret +end +function copydata{DataType}(image_out::ImageArray{DataType},image_in::ImageArray{DataType}) + image_out.data = image_in.data[image_out.arrayi_range...] +end + +function permute{DataType}(img::ImageArray{DataType},perm) + img.data = permute(img.data,perm) + img.arrayi_order = img.arrayi_order[perm] +end +############################################################# function lut(pal::Vector, a) out = similar(a, eltype(pal)) From 078ae4274b025340cd1826a5676cb315c9c6ef4a Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 19 Mar 2012 09:50:59 -0500 Subject: [PATCH 05/10] Provide a deep-copy method for ImageArray. This will need some main-library changes, because currently there is no deep-copy operation for ranges. --- extras/image.jl | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/extras/image.jl b/extras/image.jl index 5093caa2fe17f..78e4a5542aebc 100644 --- a/extras/image.jl +++ b/extras/image.jl @@ -123,8 +123,9 @@ type ImageArray{DataType<:Number} <: Image metadata # arbitrary metadata, like acquisition date&time, etc. end # Empty constructor (doesn't work right now for some reason) -ImageArray{DataType<:Number}() = ImageArray{DataType}(Array(DataType,0),"",Array(Int,0),Array(Range1,0),zeros(0,0),Array(ASCIIString,0),Array(ASCIIString,0),zeros(0),"","",false,[]) -# Construct from a data array +#ImageArray{DataType<:Number}() = ImageArray{DataType}(Array(DataType,0),"",Array(Int,0),Array(Range1,0),zeros(0,0),Array(ASCIIString,0),Array(ASCIIString,0),zeros(0),"","",false,[]) +# Construct from a data array, providing defaults for everything +# except the storage order function ImageArray{DataType<:Number}(data::Array{DataType},arrayi_order::ASCIIString) sz = size(data) szv = vcat(sz...) @@ -169,6 +170,21 @@ function ImageArray{DataType<:Number}(data::Array{DataType},arrayi_order::ASCIIS ImageArray{DataType}(data,arrayi_order,szv,arrayi_range,T,physc_unit,physc_name,arrayti2physt,t_unit,color_space,true,[]) end +function copy(img::ImageArray) + ImageArray(copy(img.data), + copy(img.arrayi_order), + copy(img.data_size), + copy(img.arrayi_range), + copy(img.arrayi2physc), + copy(img.physc_unit), + copy(img.physc_name), + copy(img.arrayti2phys), + copy(img.t_unit), + copy(img.color_space), + copy(img.valid), + copy(img.metadata)) +end + function set_pixel_spacing(img::Image,dx::Vector) n_spatial_dims = size(img.arrayi2physc,1) if n_spatial_dims != length(dx) @@ -188,13 +204,13 @@ function get_pixel_spacing(img::Image) return dx end -function ref{DataType}(img::ImageArray{DataType},cv...) +function ref(img::ImageArray,cv...) if length(cv) % 2 != 0 error("Coordinate/value must come in pairs") end imgret = copy(img) # Prepare the coordinates for snipping - cc = cell(ndims(img.data)) + cc = Array(Range1{Int},ndims(img.data)) for idim = 1:ndims(img.data) cc[idim] = 1:size(img.data,idim) end From 77ca23ac12ba5802f5654c7fee42c8a4fcf46ad8 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 19 Mar 2012 12:07:03 -0500 Subject: [PATCH 06/10] Get ImageArray permute! working - added isspatial function to easily find spatial coordinates - remembered to permute the other relevant fields. The hardest part is computing the permutation to be applied to just spatial dimensions, but a little bit of indexing gymnastics seems to do the trick - Fix one typo that kept the copy operation from working - Random cleanups to documentation --- extras/image.jl | 67 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/extras/image.jl b/extras/image.jl index 78e4a5542aebc..0b3ef03eb4242 100644 --- a/extras/image.jl +++ b/extras/image.jl @@ -84,19 +84,25 @@ abstract Image # 0 0 3 0] # -# A final important task is representing valid pixels, since real-world -# imaging devices/situations can result in a subset of the data -# being untrustworthy. In cases where the underlying data type The valid field can be set to true (all -# pixels are trustworthy), a boolean matrix of the size of the data -# array (marking trustworthy pixels individually), or as a container -# of arrays whose product would be equal to the full-size valid -# pixels array. For example, +# A final important task is representing valid pixels, since +# real-world imaging devices/situations can result in a subset of +# the data being untrustworthy. In cases where the underlying data +# type has NaN, you can easily mark the invalid pixels in the data +# array itself. However, when the data type does not have NaN, the +# valid field is necessary. Note that when both are possible, the +# validity of a pixel should be evaluated as !isnan(data) & valid, +# meaning that marking a pixel as invalid by either NaN or setting +# valid false suffices. +# The valid field can be set to true (all pixels are trustworthy), a +# boolean matrix of the size of the data array (marking trustworthy +# pixels individually), or as a container of arrays whose product +# would be equal to the full-size valid pixels array. For example, # valid = [good_pixels_per_frame,good_frames] # where good_pixels_per_frame is a boolean of size [sizex sizey] and # good_frames is a boolean of size [1 1 n_frames]. -# A few utility functions: +# Utility functions: # Make sure the storage order string doesn't duplicate any names function assert_chars_unique(s::ASCIIString) ss = sort(b"$s") @@ -107,6 +113,21 @@ function assert_chars_unique(s::ASCIIString) end end +function isspatial(s::ASCIIString) +# this returns a vector of bools, is it OK to have the name start +# with "is"? + indx = trues(length(s)) + matcht = match(r"t",s) + if !is(matcht,nothing) + indx[matcht.offset] = false; + end + matchc = match(r"c",s) + if !is(matchc,nothing) + indx[matchc.offset] = false; + end + return indx +end + # An image type with all data held in memory type ImageArray{DataType<:Number} <: Image data::Array{DataType} # the raw data @@ -122,8 +143,20 @@ type ImageArray{DataType<:Number} <: Image valid # which pixels can be trusted? metadata # arbitrary metadata, like acquisition date&time, etc. end -# Empty constructor (doesn't work right now for some reason) -#ImageArray{DataType<:Number}() = ImageArray{DataType}(Array(DataType,0),"",Array(Int,0),Array(Range1,0),zeros(0,0),Array(ASCIIString,0),Array(ASCIIString,0),zeros(0),"","",false,[]) +# Empty constructor (doesn't seem to work now, for unknown reason) +ImageArray{DataType<:Number}() = + ImageArray{DataType}(Array(DataType,0), + "", + Array(Int,0), + Array(Range1,0), + zeros(0,0), + Array(ASCIIString,0), + Array(ASCIIString,0), + zeros(0), + "", + "", + false, + "") # Construct from a data array, providing defaults for everything # except the storage order function ImageArray{DataType<:Number}(data::Array{DataType},arrayi_order::ASCIIString) @@ -167,7 +200,7 @@ function ImageArray{DataType<:Number}(data::Array{DataType},arrayi_order::ASCIIS color_space = "CMYK" end end - ImageArray{DataType}(data,arrayi_order,szv,arrayi_range,T,physc_unit,physc_name,arrayti2physt,t_unit,color_space,true,[]) + ImageArray{DataType}(data,arrayi_order,szv,arrayi_range,T,physc_unit,physc_name,arrayti2physt,t_unit,color_space,true,"") end function copy(img::ImageArray) @@ -178,7 +211,7 @@ function copy(img::ImageArray) copy(img.arrayi2physc), copy(img.physc_unit), copy(img.physc_name), - copy(img.arrayti2phys), + copy(img.arrayti2physt), copy(img.t_unit), copy(img.color_space), copy(img.valid), @@ -228,8 +261,16 @@ function copydata{DataType}(image_out::ImageArray{DataType},image_in::ImageArray image_out.data = image_in.data[image_out.arrayi_range...] end -function permute{DataType}(img::ImageArray{DataType},perm) +function permute!{DataType}(img::ImageArray{DataType},perm) img.data = permute(img.data,perm) + img.data_size = img.data_size[perm] + img.arrayi_range = img.arrayi_range[perm] + # Permute arrayi2physc: first compute the spatial permutation + flag = isspatial(img.arrayi_order) + cflag = cumsum(int(flag)) + perm_spatial = cflag[perm[flag[perm]]] + img.arrayi2physc = [img.arrayi2physc[:,perm_spatial], img.arrayi2physc[:,end]] + # Finally, permute the storage order string img.arrayi_order = img.arrayi_order[perm] end From fd56cfcee14fc93d30ea345afc594d7ed2b701e2 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 19 Mar 2012 12:22:52 -0500 Subject: [PATCH 07/10] Delete comma (typo that causes error) This also cleans up a few of the comments further --- extras/image.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/extras/image.jl b/extras/image.jl index 0b3ef03eb4242..da17b87e9dd53 100644 --- a/extras/image.jl +++ b/extras/image.jl @@ -23,7 +23,8 @@ abstract Image # x-coordinate. This would work no matter how img is stored; for # this example, it is equivalent to # imsnip.data = img.data[:,4,:]. -# ref is used instead of slicedim because the following also works: +# The syntax is ref rather than slicedim because the following also +# works: # imsnip = ref(img,'x',3:23,'y',100:200) # # There are two reserved choices for the array dimension names: 't' @@ -132,7 +133,7 @@ end type ImageArray{DataType<:Number} <: Image data::Array{DataType} # the raw data arrayi_order::ASCIIString # storage order of data array, e.g. "yxc" - data_size::Vector{Int} # full size of the original array + data_size::Vector{Int} # size of the _original_ array (pre-snip) arrayi_range::Vector{Range1{Int}} # vector of ranges (snipping out blocks) arrayi2physc::Matrix{Float64} # transform matrix physc_unit::Vector # vector of strings, e.g., "microns" @@ -269,7 +270,7 @@ function permute!{DataType}(img::ImageArray{DataType},perm) flag = isspatial(img.arrayi_order) cflag = cumsum(int(flag)) perm_spatial = cflag[perm[flag[perm]]] - img.arrayi2physc = [img.arrayi2physc[:,perm_spatial], img.arrayi2physc[:,end]] + img.arrayi2physc = [img.arrayi2physc[:,perm_spatial] img.arrayi2physc[:,end]] # Finally, permute the storage order string img.arrayi_order = img.arrayi_order[perm] end From 18d87a396d270703cf20f44ba2735bc725e75bd1 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 19 Mar 2012 15:17:20 -0500 Subject: [PATCH 08/10] Increase user-friendliness of API - Support conventional referencing, img[20:100,...] - Add support for the "size" function - Clean up the documentation - Don't deep-copy references, they are immutable (Jeff B) This passes a simple unit test function I maintain here. --- extras/image.jl | 111 ++++++++++++++++++++++++++++++------------------ 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/extras/image.jl b/extras/image.jl index da17b87e9dd53..9b521b968c256 100644 --- a/extras/image.jl +++ b/extras/image.jl @@ -16,16 +16,11 @@ abstract Image # introduces a one-character name for each array dimension. For # example, one could create an image out of a data array A[y,x,c] as # img = ImageArray(A,"yxc") -# Images can be manipulated in terms of these coordinate names, -# e.g., -# imsnip = ref(img,'x',4) -# would yield an image called imsnip that is a slice of im along the -# x-coordinate. This would work no matter how img is stored; for -# this example, it is equivalent to -# imsnip.data = img.data[:,4,:]. -# The syntax is ref rather than slicedim because the following also -# works: -# imsnip = ref(img,'x',3:23,'y',100:200) +# Images can then be manipulated in "ordinary" ways, +# imgsnip = img[50:200,100:175,1:3] +# but also in terms of these coordinate names, e.g., +# imsnip = img['x',100:175,'y',50:200] +# which would yield the same result. # # There are two reserved choices for the array dimension names: 't' # (for time) and 'c' (for channel). All other choices are assumed to @@ -38,9 +33,9 @@ abstract Image # names is arbitrary. One suggestion is to choose a name that # indicates the direction of increasing array index. For example, # choosing "br" (for "bottom-right") instead of "yx" might more -# clearly signal that the lower-left corner (as displayed on the -# screen) should be A[sz1,1], and the upper-right corner is -# A[1,sz2]. In MRI, "RAS" might indicate a standard right-handed +# clearly signal that the upper-left corner (as displayed on the +# screen) should be A[1,1], and the bottom-right corner is +# A[sz1,sz2]. In MRI, "RAS" might indicate a standard right-handed # coordinate system ('R' = rightward-increasing, 'A' = # anterior-increasing, 'S' = superior-increasing). Note that "ASR" # would imply the same coordinate system but that the data are @@ -174,10 +169,7 @@ function ImageArray{DataType<:Number}(data::Array{DataType},arrayi_order::ASCIIS matchc = match(r"c",arrayi_order) n_spatial_dims = n_dims - !is(matcht,nothing) - !is(matchc,nothing) # Set up defaults for other fields - arrayi_range = Array(Range1{Int},n_dims) - for idim = 1:n_dims - arrayi_range[idim] = 1:sz[idim] - end + arrayi_range = map(x->1:x,szv) physc_unit = Array(ASCIIString,n_spatial_dims) physc_name = Array(ASCIIString,n_spatial_dims) physc_unit[1:n_spatial_dims] = "" @@ -204,11 +196,13 @@ function ImageArray{DataType<:Number}(data::Array{DataType},arrayi_order::ASCIIS ImageArray{DataType}(data,arrayi_order,szv,arrayi_range,T,physc_unit,physc_name,arrayti2physt,t_unit,color_space,true,"") end +### Copy and ref functions ### +# Deep copy function copy(img::ImageArray) ImageArray(copy(img.data), copy(img.arrayi_order), copy(img.data_size), - copy(img.arrayi_range), + img.arrayi_range, # ranges are immutable copy(img.arrayi2physc), copy(img.physc_unit), copy(img.physc_name), @@ -219,6 +213,63 @@ function copy(img::ImageArray) copy(img.metadata)) end +# Copy everything but the data and the metadata +function copy_pfields(img::ImageArray) + ImageArray(img.data, + copy(img.arrayi_order), + copy(img.data_size), + img.arrayi_range, + copy(img.arrayi2physc), + copy(img.physc_unit), + copy(img.physc_name), + copy(img.arrayti2physt), + copy(img.t_unit), + copy(img.color_space), + copy(img.valid), + img.metadata) +end + +# Copy just the data +function copydata{DataType}(image_out::ImageArray{DataType},image_in::ImageArray{DataType}) + image_out.data = image_in.data[image_out.arrayi_range...] +end + +# This supports two ref syntaxes +function ref(img::ImageArray,ind...) + if isa(ind[1],Char) + ## Named ref syntax: ref(img,'a',20:50,'b',40:200,...) + if length(ind) % 2 != 0 + error("Coordinate/value must come in pairs") + end + imgret = copy_pfields(img) + # Prepare the coordinates for snipping + sniprange = map(x->1:x,vcat(size(img.data)...)) + for iarg = 1:2:length(ind) + idim = strchr(img.arrayi_order,ind[iarg]) + if idim == 0 + error(strcat("Array index name '",ind[iarg],"' does not match any of the names in \"",img.arrayi_order,"\"")) + end + sniprange[idim] = ind[iarg+1] + end + # Do the snip + imgret.data = img.data[sniprange...] + imgret.arrayi_range = sniprange + return imgret + else + # Normal ref syntax: img[20:50,40:200,...] + imgret = copy_pfields(img) + imgret.data = ref(img.data,ind...) + imgret.arrayi_range = convert(Array{Range1{Int},1},{ind...}) + return imgret + end +end + + +### Utility functions ### +function size(img::ImageArray) + return size(img.data) +end + function set_pixel_spacing(img::Image,dx::Vector) n_spatial_dims = size(img.arrayi2physc,1) if n_spatial_dims != length(dx) @@ -238,30 +289,8 @@ function get_pixel_spacing(img::Image) return dx end -function ref(img::ImageArray,cv...) - if length(cv) % 2 != 0 - error("Coordinate/value must come in pairs") - end - imgret = copy(img) - # Prepare the coordinates for snipping - cc = Array(Range1{Int},ndims(img.data)) - for idim = 1:ndims(img.data) - cc[idim] = 1:size(img.data,idim) - end - for icv = 1:2:length(cv) - idim = strchr(img.arrayi_order,cv[icv]) - cc[idim] = cv[icv+1] - end - # Do the snip - imgret.data = img.data[cc...] - imgret.arrayi_range = cc - return imgret -end - -function copydata{DataType}(image_out::ImageArray{DataType},image_in::ImageArray{DataType}) - image_out.data = image_in.data[image_out.arrayi_range...] -end +### Manipulations function permute!{DataType}(img::ImageArray{DataType},perm) img.data = permute(img.data,perm) img.data_size = img.data_size[perm] From 60ed6ea076e3467a7dd8429441ed221a4bd56caa Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 19 Mar 2012 15:44:23 -0500 Subject: [PATCH 09/10] Merge branch 'HEAD', remote branch 'origin' From 23e82f5023cd83b03ccea005107a691b6e610d00 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Tue, 20 Mar 2012 22:07:44 -0500 Subject: [PATCH 10/10] Make changes suggested by Stefan Kroboth, and added new ref & assign methods. Details: - Value range field added - Renamed data_size, but changed it to size_ancestor, hoping that is clearer - Defined new types for color spaces - Defined sub and assign methods --- extras/color.jl | 24 ++++++++++ extras/image.jl | 114 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 111 insertions(+), 27 deletions(-) diff --git a/extras/color.jl b/extras/color.jl index 686b288c03959..1816372216218 100644 --- a/extras/color.jl +++ b/extras/color.jl @@ -174,3 +174,27 @@ const _jl_color_names = { "yellowgreen" => (154, 205, 50), } +## Color spaces +abstract ColorSpace +type CSnil <: ColorSpace +end +type CSgray <: ColorSpace +end +type CSsRGB <: ColorSpace +end +type CSCMYK <: ColorSpace +end +# Color space where each channel is given a name, e.g., +# cs = CSNamed("GFP","tdTomato") +# or +# cs = CSNamed(["GFP","tdTomato"]) +type CSNamed <: ColorSpace + str::Vector{ASCIIString} +end +CSNamed(t...) = CSNamed([t...]) # allow tuple +function ref(n::CSNamed,ind::Int) + return n.str[ind] +end +function assign(n::CSNamed,value,key) + n.str[key] = value +end diff --git a/extras/image.jl b/extras/image.jl index 9b521b968c256..b94724063b210 100644 --- a/extras/image.jl +++ b/extras/image.jl @@ -1,3 +1,5 @@ +load("color.jl") + # The super-type of all images abstract Image @@ -124,25 +126,36 @@ function isspatial(s::ASCIIString) return indx end +getminmax(::Type{Uint8}) = [typemin(Uint8),typemax(Uint8)] +getminmax(::Type{Uint16}) = [typemin(Uint16),typemax(Uint16)] +getminmax(::Type{Uint32}) = [typemin(Uint32),typemax(Uint32)] +getminmax(::Type{Int8}) = [typemin(Int8),typemax(Int8)] +getminmax(::Type{Int16}) = [typemin(Int16),typemax(Int16)] +getminmax(::Type{Int32}) = [typemin(Int32),typemax(Int32)] +getminmax(::Type{Float32}) = [typemin(Float32),typemax(Float32)] +getminmax(::Type{Float64}) = [typemin(Float64),typemax(Float64)] + # An image type with all data held in memory type ImageArray{DataType<:Number} <: Image data::Array{DataType} # the raw data arrayi_order::ASCIIString # storage order of data array, e.g. "yxc" - data_size::Vector{Int} # size of the _original_ array (pre-snip) + minmax::Vector{DataType} # min and max possible values + size_ancestor::Vector{Int} # size of the _original_ array (pre-snip) arrayi_range::Vector{Range1{Int}} # vector of ranges (snipping out blocks) arrayi2physc::Matrix{Float64} # transform matrix physc_unit::Vector # vector of strings, e.g., "microns" physc_name::Vector # vector of strings, like "X" or "horizontal" arrayti2physt::Vector{Float64}# time coordinate lookup table t_unit::String # time coordinate unit string - color_space # string or vector of strings, e.g. "sRGB" + color_space # object of type ColorSpace valid # which pixels can be trusted? metadata # arbitrary metadata, like acquisition date&time, etc. end # Empty constructor (doesn't seem to work now, for unknown reason) -ImageArray{DataType<:Number}() = +ImageArray{DataType<:Number}() = ImageArray{DataType}(Array(DataType,0), "", + getminmax(DataType), Array(Int,0), Array(Range1,0), zeros(0,0), @@ -183,26 +196,27 @@ function ImageArray{DataType<:Number}(data::Array{DataType},arrayi_order::ASCIIS arrayti2physt = zeros(0) t_unit = "" end - color_space = "" + color_space = CSnil if !is(matchc,nothing) if size(data,matchc.offset) == 1 - color_space = "gray" + color_space = CSgray elseif size(data,matchc.offset) == 3 - color_space = "sRGB" + color_space = CSsRGB elseif szv[matchc.offset] == 4 - color_space = "CMYK" + color_space = CSCMYK end end - ImageArray{DataType}(data,arrayi_order,szv,arrayi_range,T,physc_unit,physc_name,arrayti2physt,t_unit,color_space,true,"") + ImageArray{DataType}(data,arrayi_order,getminmax(DataType),szv,arrayi_range,T,physc_unit,physc_name,arrayti2physt,t_unit,color_space,true,"") end ### Copy and ref functions ### -# Deep copy +# Deep copy---copies everything that is immutable function copy(img::ImageArray) ImageArray(copy(img.data), copy(img.arrayi_order), - copy(img.data_size), - img.arrayi_range, # ranges are immutable + copy(img.minmax), + copy(img.size_ancestor), + img.arrayi_range, # ranges are immutable, or will be copy(img.arrayi2physc), copy(img.physc_unit), copy(img.physc_name), @@ -217,7 +231,8 @@ end function copy_pfields(img::ImageArray) ImageArray(img.data, copy(img.arrayi_order), - copy(img.data_size), + copy(img.minmax), + copy(img.size_ancestor), img.arrayi_range, copy(img.arrayi2physc), copy(img.physc_unit), @@ -230,27 +245,39 @@ function copy_pfields(img::ImageArray) end # Copy just the data -function copydata{DataType}(image_out::ImageArray{DataType},image_in::ImageArray{DataType}) +function copy_data{DataType}(image_out::ImageArray{DataType},image_in::ImageArray{DataType}) image_out.data = image_in.data[image_out.arrayi_range...] end +# Copy just the metadata +function copy_metadata{DataType}(image_out::ImageArray{DataType},image_in::ImageArray{DataType}) + image_out.metadata = copy(image_in.metadata) +end + +# Private function for converting name/value lists into indices +function _image_named_coords_sub(img::Image,ind...) + if length(ind) % 2 != 0 + println(ind...) + error("Coordinate/value must come in pairs") + end + # Prepare the coordinates + sniprange = map(x->1:x,vcat(size(img.data)...)) + for iarg = 1:2:length(ind) + idim = strchr(img.arrayi_order,ind[iarg]) + if idim == 0 + error(strcat("Array index name '",ind[iarg],"' does not match any of the names in \"",img.arrayi_order,"\"")) + end + sniprange[idim] = ind[iarg+1] + end + return sniprange +end + # This supports two ref syntaxes function ref(img::ImageArray,ind...) if isa(ind[1],Char) ## Named ref syntax: ref(img,'a',20:50,'b',40:200,...) - if length(ind) % 2 != 0 - error("Coordinate/value must come in pairs") - end imgret = copy_pfields(img) - # Prepare the coordinates for snipping - sniprange = map(x->1:x,vcat(size(img.data)...)) - for iarg = 1:2:length(ind) - idim = strchr(img.arrayi_order,ind[iarg]) - if idim == 0 - error(strcat("Array index name '",ind[iarg],"' does not match any of the names in \"",img.arrayi_order,"\"")) - end - sniprange[idim] = ind[iarg+1] - end + sniprange = _image_named_coords_sub(img,ind...) # Do the snip imgret.data = img.data[sniprange...] imgret.arrayi_range = sniprange @@ -259,8 +286,41 @@ function ref(img::ImageArray,ind...) # Normal ref syntax: img[20:50,40:200,...] imgret = copy_pfields(img) imgret.data = ref(img.data,ind...) - imgret.arrayi_range = convert(Array{Range1{Int},1},{ind...}) + for i = 1:length(ind) + imgret.arrayi_range[i] = ind[i] + end + return imgret + end +end +function sub(img::ImageArray,ind...) + if isa(ind[1],Char) + ## Named sub syntax: sub(img,'a',20:50,'b',40:200,...) + imgret = copy_pfields(img) + sniprange = _image_named_coords_sub(img,ind) + # Do the snip + imgret.data = sub(img.data,sniprange...) + imgret.arrayi_range = sniprange return imgret + else + # Normal sub syntax: img[20:50,40:200,...] + imgret = copy_pfields(img) + imgret.data = sub(img.data,ind...) + for i = 1:length(ind) + imgret.arrayi_range[i] = ind[i] + end + return imgret + end +end +function assign(img::ImageArray,val,ind...) + if isa(ind[1],Char) + ## Named assign syntax: assign(img,'a',20:50,'b',40:200,...) + sniprange = _image_named_coords_sub(img,ind) + # Do the snip + img.data[sniprange...] = val + else + # Normal assign syntax: img[20:50,40:200,...] + imgret = copy_pfields(img) + img.data[ind...] = val end end @@ -293,7 +353,7 @@ end ### Manipulations function permute!{DataType}(img::ImageArray{DataType},perm) img.data = permute(img.data,perm) - img.data_size = img.data_size[perm] + img.size_ancestor = img.size_ancestor[perm] img.arrayi_range = img.arrayi_range[perm] # Permute arrayi2physc: first compute the spatial permutation flag = isspatial(img.arrayi_order)