require_relative '../ffi/gdal' require_relative 'color_table' require_relative 'major_object' require_relative 'raster_attribute_table' require 'narray' module GDAL class RasterBand include FFI::GDAL include MajorObject attr_reader :dataset # @param dataset [GDAL::Dataset, FFI::Pointer] # @param band_id [Fixnum] Requried if not passing in +raster_band_pointer+. # @param raster_band_pointer [FFI::Pointer] Requried if not passing in # +band_id+. def initialize(dataset, band_id: nil, raster_band_pointer: nil) if dataset.is_a? GDAL::Dataset @dataset = dataset.c_pointer else @dataset = dataset end @gdal_raster_band = if raster_band_pointer raster_band_pointer elsif band_id GDALGetRasterBand(@dataset, band_id) else raise 'Must pass in band_id or the raster_band_pointer.' end end def c_pointer @gdal_raster_band end # The raster width in pixels. # # @return [Fixnum] def x_size return nil if null? GDALGetRasterBandXSize(@gdal_raster_band) end # The raster height in pixels. # # @return [Fixnum] def y_size return nil if null? GDALGetRasterBandYSize(@gdal_raster_band) end # The type of access to the raster band this object currently has. # # @return [Symbol] Either :GA_Update or :GA_ReadOnly. def access_flag return nil if null? GDALGetRasterAccess(@gdal_raster_band) end # The number of band within the associated dataset that this band # represents. # # @return [Fixnum] def band_number return nil if null? GDALGetBandNumber(@gdal_raster_band) end # @return [Symbol] One of FFI::GDAL::GDALColorInterp. def color_interpretation GDALGetRasterColorInterpretation(@gdal_raster_band) end # @return [GDAL::ColorTable] def color_table gdal_color_table = GDALGetRasterColorTable(@gdal_raster_band) return nil if gdal_color_table.null? @color_table ||= ColorTable.new(@gdal_raster_band, color_table_pointer: gdal_color_table) end # The pixel data type for this band. # # @return [Symbol] One of FFI::GDAL::GDALDataType. def data_type GDALGetRasterDataType(@gdal_raster_band) end # The natural block size is the block size that is most efficient for # accessing the format. For many formats this is simply a whole scanline # in which case x is set to #x_size, and y is set to 1. # # @return [Hash{x => Fixnum, y => Fixnum}] def block_size x_pointer = FFI::MemoryPointer.new(:int) y_pointer = FFI::MemoryPointer.new(:int) GDALGetBlockSize(@gdal_raster_band, x_pointer, y_pointer) { x: x_pointer.read_int, y: y_pointer.read_int } end # @return [Array] def category_names names = GDALGetRasterCategoryNames(@gdal_raster_band) return [] if names.null? names.get_array_of_string(0) end # @param names [Array] # @return [Array] def category_names=(names) str_pointers = names.map do |name| FFI::MemoryPointer.from_string(name.to_s) end str_pointers << nil names_pointer = FFI::MemoryPointer.new(:pointer, str_pointers.length) str_pointers.each_with_index do |ptr, i| names_pointer[i].put_pointer(0, ptr) end cpl_err = GDALSetRasterCategoryNames(@gdal_raster_band, names_pointer) cpl_err.to_ruby(warning: []) end # The no data value for a band is generally a special marker value used to # mark pixels that are not valid data. Such pixels should generally not be # displayed, nor contribute to analysis operations. # # @return [Hash{value => Float, is_associated => Boolean}] def no_data_value associated = FFI::MemoryPointer.new(:bool) value = GDALGetRasterNoDataValue(@gdal_raster_band, associated) { value: value, is_associated: associated.read_bytes(1).to_bool } end # @return [Fixnum] def overview_count GDALGetOverviewCount(@gdal_raster_band) end # @return [Boolean] def arbitrary_overviews? GDALHasArbitraryOverviews(@gdal_raster_band).zero? ? false : true end # @param index [Fixnum] Must be between 0 and (#overview_count - 1). # @return [GDAL::RasterBand] def overview(index) return nil if overview_count.zero? overview_pointer = GDALGetOverview(@gdal_raster_band, index) return nil if overview_pointer.null? self.class.new(dataset, raster_band_pointer: overview_pointer) end # @param desired_samples [Fixnum] The returned band will have at least this # many pixels. # @return [GDAL::RasterBand] An optimal overview or the same raster band if # the raster band has no overviews. def raster_sample_overview(desired_samples=0) band_pointer = GDALGetRasterSampleOverview(@gdal_raster_band, desired_samples) return nil if band_pointer.null? self.class.new(dataset, raster_band_pointer: band_pointer) end # @return [GDAL::RasterBand] def mask_band band_pointer = GDALGetMaskBand(@gdal_raster_band) return nil if band_pointer.null? self.class.new(dataset, raster_band_pointer: band_pointer) end # @return [Array] def mask_flags flag_list = GDALGetMaskFlags(@gdal_raster_band).to_s(2).scan(/\d/) flags = [] flag_list.reverse.each_with_index do |flag, i| if i == 0 && flag.to_i == 1 flags << :GMF_ALL_VALID elsif i == 1 && flag.to_i == 1 flags << :GMF_PER_DATASET elsif i == 2 && flag.to_i == 1 flags << :GMF_ALPHA elsif i == 3 && flag.to_i == 1 flags << :GMF_NODATA end end flags end # Returns minimum, maximum, mean, and standard deviation of all pixel values # in this band. # # @param approx_ok [Boolean] If +true+, stats may be computed based on # overviews or a subset of all tiles. # @param force [Boolean] If +false+, stats will only be returned if the # calculating can be done without rescanning the image. # @return [Hash{mininum: Float, maximum: Float, mean: Float, # standard_deviation: Float}] def statistics(approx_ok=true, force=true) min = FFI::MemoryPointer.new(:double) max = FFI::MemoryPointer.new(:double) mean = FFI::MemoryPointer.new(:double) standard_deviation = FFI::MemoryPointer.new(:double) cpl_err = GDALGetRasterStatistics(@gdal_raster_band, approx_ok, force, min, max, mean, standard_deviation) minimum = min.null? ? 0.0 : min.read_double case cpl_err.to_ruby when :none, :debug { minimum: min.read_double, maximum: max.read_double, mean: mean.read_double, standard_deviation: standard_deviation.read_double } when :warning then {} when :failure, :fatal then raise CPLErrFailure end end # The raster value scale. This value (in combination with the #offset # value) is used to transform raw pixel values into the units returned by # #units. For example this might be used to store elevations in GUInt16 # bands with a precision of 0.1, and starting from -100. # # Units value = (raw value * scale) + offset # # For file formats that don't know this intrinsically a value of one is # returned. # # @return [Hash{value => Float, is_meaningful => Boolean}] def scale meaningful = FFI::MemoryPointer.new(:bool) result = GDALGetRasterScale(@gdal_raster_band, meaningful) { value: result, is_meaningful: meaningful.read_bytes(1).to_bool } end # @param new_scale [Float] # @return [FFI::GDAL::CPLErr] def scale=(new_scale) GDALSetRasterScale(@gdal_raster_band, new_scale.to_f) end # This value (in combination with the GetScale() value) is used to # transform raw pixel values into the units returned by #units. For example # this might be used to store elevations in GUInt16 bands with a precision # of 0.1, and starting from -100. # # Units value = (raw value * scale) + offset. # # For file formats that don't know this intrinsically a value of 0.0 is # returned. # # @return [Hash{value => Float, is_meaningful => Boolean}] def offset meaningful = FFI::MemoryPointer.new(:bool) result = GDALGetRasterOffset(@gdal_raster_band, meaningful) { value: result, is_meaningful: meaningful.read_bytes(1).to_bool } end # @param new_offset [Float] # @return [FFI::GDAL::CPLErr] def offset=(new_offset) GDALSetRasterOffset(@gdal_raster_band, new_offset) end # @return [String] def unit_type GDALGetRasterUnitType(@gdal_raster_band) end # @param new_unit_type [String] "" indicates unknown, "m" is meters, "ft" # is feet; other non-standard values are allowed. # @return [FFI::GDAL::CPLErr] def unit_type=(new_unit_type) GDALSetRasterUnitType(@gdal_raster_band, new_unit_type) end # Gets the default raster histogram. Results are returned as a Hash so some # metadata about the histogram can be returned. Example: # # { # :mininum => -0.9, # :maximum => 255.9, # :buckets => 256, # :totals => [ # 3954, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 0, # 0, 0, 10, 27, 201, 699, 1766, 3472, 5013, 6464, 7698, 8352, # 9039, 10054, 11378, 13132, 14377, 14371, 14221, 14963, 14740, # 14379, 13724, 12938, 11318, 9828, 8504, 7040, 5700, 4890, # 4128, 3276, 2749, 2322, 1944, 1596, 1266, 1050, 784, 663, # 547, 518, 367, 331, 309, 279, 178, 169, 162, 149, 109, 98, # 90, 89, 82, 85, 74, 75, 42, 40, 39, 35, 39, 36, 36, 27, 20, # 12, 13, 19, 16, 12, 11, 6, 6, 8, 12, 6, 8, 11, 3, 7, 9, 2, # 5, 2, 5, 1, 4, 0, 0, 1, 0, 1, 2, 1, 0, 2, 1, 0, 0, 1, 0, 1, # 1, 1, 0, 2, 1, 2, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 # ] # } # # @param force [Boolean] Forces the computation of the histogram. If # +false+ and the default histogram isn't available, this returns nil. # @param block [Proc] No required, but can be used to output progess info # during processing. # @yieldparam completion [Float] The ration completed as a decimal. # @yieldparam message [String] Message string to display. # @return [Hash{minimum => Float, maximum => Float, buckets => Fixnum, # totals => Array}] def default_histogram(force=false, &block) min_pointer = FFI::MemoryPointer.new(:double) max_pointer = FFI::MemoryPointer.new(:double) buckets_pointer = FFI::MemoryPointer.new(:int) histogram_pointer = FFI::MemoryPointer.new(:pointer) progress_proc = block || Proc.new do |completion, message, progress_arg| puts "completion: #{completion * 100}" puts "message: #{message}" true end cpl_err = GDALGetDefaultHistogram(@gdal_raster_band, min_pointer, max_pointer, buckets_pointer, histogram_pointer, force, progress_proc, nil ) min = min_pointer.read_double max = max_pointer.read_double buckets = buckets_pointer.read_int totals = histogram_pointer.get_pointer(0).read_array_of_int(buckets) case cpl_err.to_ruby when :none, :debug { minimum: min, maximum: max, buckets: buckets, totals: totals } when :warning then return nil when :failure, :fatal then raise CPLError end end # @return [GDAL::RasterAttributeTable] def default_raster_attribute_table rat_pointer = GDALGetDefaultRAT(c_pointer) return nil if rat_pointer.null? GDAL::RasterAttributeTable.new(c_pointer, raster_attribute_table_pointer: rat_pointer) end # Computes a histogram using the given inputs. If you just want the default # histogram, use #default_histogram. # # @param min [Float] # @param max [Float] # @param buckets [Fixnum] # @param include_out_of_range [Boolean] # @param approx_ok [Boolean] # @param block [Proc] No required, but can be used to output progess info # during processing. # @yieldparam completion [Float] The ration completed as a decimal. # @yieldparam message [String] Message string to display. # @return [Hash{minimum => Float, maximum => Float, buckets => Fixnum, # totals => Array}] def histogram(min, max, buckets, include_out_of_range: false, approx_ok: true, &block) histogram_pointer = FFI::MemoryPointer.new(:int, buckets) progress_proc = block || Proc.new do |completion, message, progress_arg| puts "progress: #{completion * 100}" true end cpl_err = GDALGetRasterHistogram(@gdal_raster_band, min.to_f, max.to_f, buckets, histogram_pointer, include_out_of_range, approx_ok, progress_proc, 'doing things' ) totals = histogram_pointer.read_array_of_int(0) case cpl_err.to_ruby when :none { minimum: min, maximum: max, buckets: buckets, totals: totals } when :warning then return nil when :failure then raise CPLError end end # TODO: Something about the pointer allocation smells here... #def read(x_offset: 0, y_offset: 0, x_size: x_size, y_size: 1, pixel_space: 0, line_space: 0) def readlines x_offset = 0 line_size = 1 pixel_space = 0 line_space = 0 scan_line = FFI::MemoryPointer.new(:float, x_size) 0.upto(y_size - 1) do |y| GDALRasterIO(@gdal_raster_band, :GF_Read, x_offset, y, x_size, line_size, scan_line, x_size, line_size, data_type, pixel_space, line_space ) yield scan_line.read_array_of_float(x_size).dup end end # @param pixel_array [NArray] The NArray of pixels. # @param x_offset [Fixnum] The left-most pixel to start writing. # @param y_offset [Fixnum] The top-most pixel to start writing. # @param data_type [FFI::GDAL::GDALDataType] The type of pixel contained in # the +pixel_array+. # @param line_space [Fixnum] # @param pixel_space [Fixnum] # TODO: Write using #buffer_size to write most efficiently. # TODO: Return a value! def write_array(pixel_array, x_offset: 0, y_offset: 0, data_type: :GDT_Byte, line_space: 0, pixel_space: 0) line_size = 1 x_size = pixel_array.sizes.first y_size = pixel_array.sizes.last columns_to_write = x_size - x_offset lines_to_write = y_size - y_offset scan_line = FFI::MemoryPointer.new(:float, columns_to_write) (y_offset).upto(lines_to_write - 1) do |y| pixels = pixel_array[true, y] scan_line.write_array_of_float(pixels.to_a) GDALRasterIO(@gdal_raster_band, :GF_Write, x_offset, # nXOff y, x_size, # nXSize line_size, # nYSize scan_line, # pData x_size, # nBufXSize line_size, # nBufYSize data_type, # eBufType pixel_space, # nPixelSpace line_space # nLineSpace ) end GDALFlushRasterCache(@gdal_raster_band) end # Read a block of image data, more efficiently than #read. Doesn't # resample or do data type conversion. # # @param x_offset [Fixnum] The horizontal block offset, with 0 indicating # the left-most block, 1 the next block, etc. # @param y_offset [Fixnum] The vertical block offset, with 0 indicating the # top-most block, 1 the next block, etc. def read_block(x_offset, y_offset, image_buffer=nil) image_buffer ||= FFI::MemoryPointer.new(:void) #puts "x offset: #{x_offset}" #puts "y offset: #{y_offset}" result = GDALReadBlock(@gdal_raster_band, x_offset, y_offset, image_buffer) if result == :none elsif result == :failure end end # The minimum and maximum values for this band. # # @return [Array{min => Float, max => Float}] def compute_min_max @min_max = if minimum_value[:value] && maximum_value[:value] min_max = FFI::MemoryPointer.new(:double, 2) min_max.put_array_of_double 0, [minimum_value[:value], maximum_value[:value]] GDALComputeRasterMinMax(@gdal_raster_band, 1, min_max) [min_max[0].read_double, min_max[1].read_double] else [0.0, 0.0] end end # @return [Hash{value => Float, it_tight => Boolean}] def minimum_value is_tight = FFI::MemoryPointer.new(:bool) value = GDALGetRasterMinimum(@gdal_raster_band, is_tight) { value: value, is_tight: is_tight.read_bytes(1).to_bool } end # @return [Hash{value => Float, it_tight => Boolean}] def maximum_value is_tight = FFI::MemoryPointer.new(:double) value = GDALGetRasterMaximum(@gdal_raster_band, is_tight) { value: value, is_tight: is_tight.read_bytes(1).to_bool } end # Iterates through all lines and builds an NArray of pixels. # # @return [NArray] def to_a lines = [] readlines do |line| lines << line end NArray.to_na(lines) end end end