Skip to content

DHI.Services.Rasters — Internal Developer Guide

This is a pragmatic, “you can ship with this” guide for using the DHI.Services.Rasters package from NuGet. It explains what it is, the main concepts & types, how to wire it up, and includes copy-pasteable examples.


What the package gives you

  • A common raster model (matrix of values + geo metadata) with helpers to render bitmaps, save images (PNG/JPEG/GIF/BMP/WEBP/TIFF), and compute with pixels.
  • Radar-specific abstractions (Reflectivity/Intensity), file readers for several radar formats, and services to analyze intensities/depths over time.
  • Time-series repositories backed by folders on disk with naming conventions, including ESRI ASCII grid and Delimited ASCII grid radar file collections.
  • Zone analytics (JSON-backed zones with pixel weighting) to get area-averaged intensities, plus utilities to validate and serialize zones.
  • Bias correction helpers (spatial uniform & spatially varying).
  • Reflection-based “connections” so you can create services from config at runtime (no compile-time coupling to repository types).
  • SkiaSharp interop and a simple TIFF pipeline (via Magick.NET) for lossless map exports.

Installation

dotnet add package DHI.Services.Rasters

The package depends on SkiaSharp and Magick.NET (for TIFF). They come transitively via NuGet. On Linux/macOS you may need the usual native Skia runtime dependencies.


Core building blocks

Matrix (generic raster payload)

DHI.Services.Rasters.Matrix is a lightweight raster with:

  • Size (SKSizeI width/height), Values (IList<float>), and a DateTime id.
  • Basic IO: FromFile(...), FromStream(...), ToFile(...), ToStream().
  • Convenience: GetValue(Pixel), UpdateValue(Pixel, float), min/max helpers.
  • Rendering: ToBitmap(ColorGradient)SKBitmap.

Pixels are 1-based (col,row). Pixel(1,1) is top-left.

Example

using DHI.Services.Rasters;
using SkiaSharp;

// read/write a Matrix .bin file you produce
var m = Matrix.CreateNew("matrix.bin");        // file contains [w,h,datetime, float32[]]
var value = m.GetValue(new Pixel(10, 20));
m.UpdateValue(new Pixel(10, 20), value + 1);

// colorize and save as PNG
var gradient = new ColorGradient(
  new SortedDictionary<double, SKColor> {
    { 0, SKColors.Transparent }, { 50, SKColors.Yellow }, { 100, SKColors.Red }
  });

using var bitmap = m.ToBitmap(gradient);
using var png = File.Create("matrix.png");
bitmap.SaveAs(png, RasterImageFormat.Png);

BaseRaster & IRaster

BaseRaster extends Matrix with geo metadata:

  • GeoProjectionString, GeoCenter, GeoLowerLeft/Right/UpperLeft/UpperRight
  • PixelSize (geo units per pixel)
  • PixelValueUnit (e.g., "mm/h")
  • GetGeoCoordinates(Pixel) → the pixel’s corner coordinates
  • ToBitmap() abstract: subclasses pick a default color gradient

Use this when your data has a map context.


Image IO helpers

RasterImageFormat & SKBitmapExtensions.SaveAs(...)

Save any SKBitmap with:

bitmap.SaveAs(stream, RasterImageFormat.Png);          // PNG
bitmap.SaveAs(stream, RasterImageFormat.Jpeg);         // JPEG (quality 90)
bitmap.SaveAs(stream, RasterImageFormat.Webp);         // WEBP (quality 90)
bitmap.SaveAs(stream, RasterImageFormat.Tiff, TiffCompression.Lzw); // TIFF via Magick.NET

Available TIFF compressions: None, Lzw, Deflate, Zstd.

ImagingHelpers.SaveBitmap(...)

Quick file writer (auto-chooses format from extension):

ImagingHelpers.SaveBitmap(bitmap, "plot.webp");

Color gradients (for heatmaps)

ColorGradient maps numeric thresholds to colors with interpolation. Built-in presets for radar data live in Radar.ColorGradientType.

using DHI.Services.Rasters.Radar;

var bmp = ColorGradientType.ReflectivityDefault
  .ColorGradient
  .ToBitmap(height: 200, width: 30);

ImagingHelpers.SaveBitmap(bmp, "legend.png");

Radar model (Reflectivity/Intensity)

Key types

  • IRadarImage (extends IRaster)
    • PixelValueType (Reflectivity | Intensity)
    • GetIntensity(Pixel) (converts reflectivity to intensity if needed)
    • ToIntensity(...) to convert the whole image
  • BaseRadarImage implements conversion defaults:
    • Marshall-Palmer conversion (A=200, B=1.6) by default
    • Units default to mm/h (you can switch to µm/s)
  • PixelValueTypeReflectivity (dBZ) and Intensity (float)
  • ConversionCoefficients – customize Reflectivity→Intensity conversion
  • Funcs.NoData (float.MinValue) – sentinel for missing data

Reflectivity range guard: for conversion, valid dBZ is [-35, 65].

Example: Convert a radar snapshot to intensity and render

using DHI.Services.Rasters.Radar.X00; // DHI LAWR X00
using DHI.Services.Rasters.Radar;

var img = Radar.X00.RadarImage.CreateNew("20240101_1200.x00");

// to mm/h (default coefficients)
var intensityImg = img.ToIntensity();

// paint with default intensity gradient
using var bmp = intensityImg.ToBitmap();
bmp.SaveAs(File.Create("intensity.png"), RasterImageFormat.Png);

Radar repositories (time-series on disk)

When your radar images are stored in a folder with a consistent naming pattern, use a repository to query by time.

All repositories derive from:

BaseRadarImageRepository<TImage> : IRasterRepository<TImage>

and take a connection string:

<folderPath>;<filePattern>;<dateTimeFormat>

Where:

  • folderPath: e.g. C:\data\radar
  • filePattern: E.g. Radar33{datetimeFormat}.ascii
  • dateTimeFormat: e.g. yyyyMMddHHmmss
    • You can embed counters:
      • $$$ = “hours since base date”
      • ### = “days since base date” Example: yyyyMMdd_$$$ → file name contains base date and hour-offset

The repository scans the folder, extracts timestamps from file names, and exposes Contains(DateTime), FirstDateTime(), LastDateTime(), Get(from,to), GetFirstAfter(...), GetLastBefore(...), etc.

Supported file families out-of-the-box

  • ESRI ASCII grids: Radar.ESRIASCII.EsriAsciiRepository
  • Delimited ASCII grids (row/col of floats): Radar.DELIMITEDASCII.DelimitedAsciiRepository

Readers for IRIS CAPPI and DHI LAWR X00 formats are provided (Radar.IRISCAPPI.RadarImage, Radar.X00.RadarImage), so you can easily make your own repository by subclassing BaseRadarImageRepository<RadarImage> if you have a folder series. (See “Extend with your own repository”.)

Example: ESRI ASCII grid folder

using DHI.Services.Rasters.Radar;
using DHI.Services.Rasters.Radar.ESRIASCII;

// Connection string: folder;pattern;format
var repo = new EsriAsciiRepository(
  @"D:\radar\esri",
  "Radar33{datetimeFormat}.asc",
  "yyyyMMddHHmmss");

// Wrap in a service to get time-series analytics
var svc = new RadarImageService<Radar.ESRIASCII.AsciiImage>(repo);

// Get latest and render
var latest = svc.Last();
using var bmp = latest.ToBitmap();
bmp.SaveAs(File.Create("latest.png"), RasterImageFormat.Png);

// Sample time-window
var from = svc.LastDateTime().AddHours(-6);
var to   = svc.LastDateTime();
var images = svc.Get(from, to); // SortedDictionary<DateTime, AsciiImage>

Radar analytics — RadarImageService<TImage>

Adds high-level operations on top of a repository:

  • Intensity time series for a Zone:
    • GetIntensities(zone, from, to, [coeffs])SortedDictionary<DateTime,double>
    • GetAverageIntensity(...), GetMaxIntensity(...), GetMinIntensity(...)
  • Depth (mm) over time via trapezoidal integration:
    • GetDepth(zone, from, to)
  • Bias correction at retrieval time:
    • GetCorrected(dateTime, BiasCorrectionService)

By default, analysis windows must be ≤ 30 days (_maxAnalysisTimeSpan) and batch retrieval windows ≤ 1 day (MaxTimeSpan). Both are configurable via constructor overloads.

Example: 24h intensity curve for a zone

using DHI.Services.Rasters.Radar;
using DHI.Services.Rasters.Radar.ESRIASCII;
using DHI.Services.Rasters.Zones;

// repo → service
var repo = new EsriAsciiRepository(@"D:\radar\esri", "R_{datetimeFormat}.asc", "yyyyMMddHHmm");
var svc  = new RadarImageService<Radar.ESRIASCII.AsciiImage>(repo);

// load a zone from JSON (see ZoneRepository below), or build in code
var zone = new Zone("Z1","Catchment")
{
  ImageSize = new SKSizeI(800, 600),
  PixelWeights = new HashSet<PixelWeight> {
    new(new Pixel(100,120), new Weight(0.4)),
    new(new Pixel(101,120), new Weight(0.6)),
  }
};

// query last 24 hours
var to   = svc.LastDateTime();
var from = to.AddHours(-24);

var curve = svc.GetIntensities(zone, from, to); // Date->mm/h
var avg   = svc.GetAverageIntensity(zone, from, to); 
var max   = svc.GetMaxIntensity(zone, from, to);
var depth = svc.GetDepth(zone, from, to); // mm

Zones (area aggregation)

A Zone is a set of pixel weights that sum to \~1.0 (±0.001), used to compute area-weighted intensities from a radar image. Zones can be stored as JSON using the ZoneRepository.

Key APIs

  • Zone
    • PixelWeights : HashSet<PixelWeight>
    • ImageSize : SKSizeI
    • Type : ZoneType (Point / LineString / Polygon) – informational
    • Validation: PixelWeightsAreValid, PixelWeightTotal
    • ToBitmap(...) to visualize the mask
  • ZoneRepository (JSON file)
    • Get(id), GetAll(), Contains(id), ContainsName(name)
  • ZoneService
    • Adds validation rules on Add (unique name, weights ≈ 1.0)

JSON (de)serialization

We ship custom converters:

  • ZoneConverter for a single zone
  • ZoneDictionaryConverter for a dictionary of zones
  • ZoneTypeConverter for ZoneType

Metadata: zones/entities support a Metadata dictionary. Our MetadataConverter reads primitives (number, string, bool, null) but its Write is intentionally empty (no-op). Treat metadata as read-only when round-tripping with these converters.

Example: load/save zones

using DHI.Services.Rasters.Zones;
using System.Text.Json;

// Setup repository with converters
var options = new JsonSerializerOptions();
options.Converters.Add(new ZoneConverter());
options.Converters.Add(new ZoneDictionaryConverter());
options.Converters.Add(new ZoneTypeConverter());

var repo = new ZoneRepository(@"C:\zones\zones.json", new[] {
    new ZoneConverter(), new ZoneDictionaryConverter(), new ZoneTypeConverter()
});

// Read zones
foreach (var z in repo.GetAll())
{
  Console.WriteLine($"{z.Id}: {z.Name}, valid={z.PixelWeightsAreValid}");
}

// Add a new zone (weights must sum to 1.0)
var z1 = new Zone("Z1","PumpStation", ZoneType.Point) {
  ImageSize = new SKSizeI(800, 600),
};
z1.PixelWeights.Add(new PixelWeight(new Pixel(10,20), new Weight(1.0)));

var svc = new ZoneService(repo);
svc.Add(z1); // enforces unique name + weights≈1.0

Bias correction

BiasCorrectionService wraps an IUpdatableRepository<Matrix, DateTime> of correction matrices and offers:

  • GetLastBefore(DateTime) → last matrix prior to a timestamp
  • Static builders:
    • Spatially varying (Jensen, 2015) from IEnumerable<Gauge>
    • Spatially uniform mean field bias from IEnumerable<GaugeRadarDepth>

Use image.Correct(matrix) to apply the factors (per-pixel multiply).

Example: get corrected image

using DHI.Services.Rasters.Radar;

// Assume you have a repository of matrices (e.g., JSON, DB, or your own)
IUpdatableRepository<Matrix, DateTime> corrRepo = /* ... */;

var bias = new BiasCorrectionService(corrRepo);
var corrected = svc.GetCorrected(dateTime: new DateTime(2025, 01, 01, 12, 0, 0), bias);

Format readers

ESRI ASCII grid (Radar.ESRIASCII.AsciiImage)

  • Reads ESRI ASCII + no-data mapping to Funcs.NoData
  • Populates Size, PixelSize (square cells), Values
  • PixelValueType = Intensity (mm/h by default unless you convert units)

Delimited ASCII (Radar.DELIMITEDASCII.AsciiImage)

  • Reads 2D float arrays (space or comma delimited), flips vertical order to top-origin convention
  • Sets PixelValueType = Intensity

IRIS CAPPI (Radar.IRISCAPPI.RadarImage)

  • Reads binary IRIS outputs (CAPPI)
  • Sets PixelValueType to Reflectivity (code 2) or Intensity (code 13)
  • Fills geo center from radar lat/lon in product metadata
  • Sets PixelSize from product scales

LAWR X00 (Radar.X00.RadarImage)

  • Reads DHI LAWR .x00 headers + payload
  • Signal type determines reflectivity (byte) or intensity (double)
  • Converts reflectivity bytes using header slope/offset/ord

Note: We provide repository classes for ESRI ASCII & Delimited ASCII out of the box. For IRIS/X00 series on disk, implement your own BaseRadarImageRepository<RadarImage> (a \~10-line subclass — see example below).


Services (“connections”) from config

If you prefer to keep types out of compile time, use the connection helpers to create services via reflection.

RasterServiceConnection<TRaster>

using DHI.Services.Rasters;

var conn = new RasterServiceConnection<IRaster>("id", "RasterConn")
{
  RepositoryType = "MyCompany.MyRepo, MyRepoAssembly",
  ConnectionString = @"D:\rasters;R_{datetimeFormat}.asc;yyyyMMddHHmm"
};

var service = (RasterService<IRaster>)conn.Create();

RadarImageServiceConnection<TImage>

using DHI.Services.Rasters.Radar;

var conn = new RadarImageServiceConnection<IRadarImage>("id","RadarConn")
{
  RepositoryType = "DHI.Services.Rasters.Radar.ESRIASCII.EsriAsciiRepository, DHI.Services.Rasters",
  ConnectionString = @"D:\radar\esri;R_{datetimeFormat}.asc;yyyyMMddHHmm"
};

var radarService = (RadarImageService<IRadarImage>)conn.Create();

To assist building UIs, both connections expose CreateConnectionType<TConnection>() that returns a type description with the list of compatible repository types discovered at runtime (Service.GetProviderTypes<...>()).


Working with color ramps

Use preset ramps:

  • ColorGradientType.IntensityDefault
  • ColorGradientType.IntensityLightYellowToRed
  • ColorGradientType.IntensityLightYellowToRedLogarithmic
  • ColorGradientType.ReflectivityDefault
  • ColorGradientType.ReflectivityLightYellowToRed

Or build your own with new ColorGradient(new SortedDictionary<double, SKColor>{ ... }, isLogarithmic: false).


System.Drawing interop

DrawingInteropExtensions lets you convert common GDI types to Skia:

using DHI.Services.Rasters;

var skSizeI = new System.Drawing.Size(800, 600).ToSKSizeI();

TIFF specifics

TIFF export is handled by Skia→PNG→Magick.NET→TIFF to support proper compression and alpha. You’ll get:

  • Correct color type (TrueColor/TrueColorAlpha)
  • Compression method (Lzw, Deflate, Zstd, or None)
  • Predictor set for LZW/ZIP to improve compression ratio

Example:

bmp.SaveAs(File.Create("map.tif"), RasterImageFormat.Tiff, TiffCompression.Zstd);

Extend with your own repository (example for IRIS CAPPI)

using DHI.Services.Rasters.Radar;

public class IrisCappiRepository
  : BaseRadarImageRepository<DHI.Services.Rasters.Radar.IRISCAPPI.RadarImage>
{
  public IrisCappiRepository(string connectionString) : base(connectionString) { }
  public IrisCappiRepository(string folder, string pattern, string format)
    : base(folder, pattern, format) { }
}

Now you can do:

var repo = new IrisCappiRepository(@"D:\iris","CAPPI_{datetimeFormat}.bin","yyyyMMddHHmm");
var svc  = new RadarImageService<DHI.Services.Rasters.Radar.IRISCAPPI.RadarImage>(repo);

Common pitfalls & how to avoid them

  • Pixel indices are 1-based. When using GetValue/UpdateValue, pass new Pixel(col,row) starting at 1.
  • Zone weights must sum to \~1.0. ZoneService.Add will reject invalid zones.
  • Time windows too large. RadarImageService defaults: analysis ≤ 30 days, batch fetch ≤ 1 day. Use the constructor overload to relax if needed.
  • NoData handling. Many readers set Funcs.NoData for invalid/missing values; your stats should ignore it.
  • Reflectivity conversion range. ReflectivityToIntensity enforces dBZ in [-35,65].
  • Folder scan assumptions. Repositories require that the folder actually contains files with the extension from your filePattern, and that file names include one contiguous {datetimeFormat} section that matches the date format you specify (with optional $$$ / ### counters).
  • Metadata serialization. MetadataConverter is read-only by design. If you need to emit metadata back to JSON, serialize your metadata yourself or provide a custom converter.

End-to-end example (putting it together)

Goal: Last 6-hour intensity time series for a catchment, charted as PNG and saved as a colorized GeoTIFF map for the latest timestamp.

using DHI.Services.Rasters;
using DHI.Services.Rasters.Radar;
using DHI.Services.Rasters.Radar.ESRIASCII;
using DHI.Services.Rasters.Zones;
using SkiaSharp;

// 1) Repository & service
var repo = new EsriAsciiRepository(
  @"D:\radar\esri", "R_{datetimeFormat}.asc", "yyyyMMddHHmm");
var svc  = new RadarImageService<Radar.ESRIASCII.AsciiImage>(repo);

// 2) Define a simple zone with 3 pixels
var zone = new Zone("catch-01", "Catchment 01") {
  ImageSize = new SKSizeI(800,600),
  PixelWeights = new HashSet<PixelWeight> {
    new(new Pixel(100,120), new Weight(0.4)),
    new(new Pixel(101,120), new Weight(0.3)),
    new(new Pixel(102,120), new Weight(0.3)),
  }
};

// 3) Query intensities for last 6h
var to   = svc.LastDateTime();
var from = to.AddHours(-6);
var ts   = svc.GetIntensities(zone, from, to); // Date->mm/h

// 4) Latest image: colorize & save TIFF (LZW)
var latest = svc.Get(to);
using var latestBmp = latest.ToBitmap(ColorGradientType.IntensityDefault.ColorGradient);
using var tiff = File.Create("latest_intensity.tif");
latestBmp.SaveAs(tiff, RasterImageFormat.Tiff, TiffCompression.Lzw);

// 5) Also keep a legend
var legend = ColorGradientType.IntensityDefault.ColorGradient.ToBitmap(200, 40);
ImagingHelpers.SaveBitmap(legend, "legend.png");

// 6) Log the average and depth for that window
var avg   = svc.GetAverageIntensity(zone, from, to);
var depth = svc.GetDepth(zone, from, to);
Console.WriteLine($"Avg mm/h: {avg:F2}, Depth mm: {depth:F1}");

Reference: important namespaces & types

  • DHI.Services.Rasters
    • Matrix, BaseRaster, IRaster, Pixel, ColorGradient, RasterImageFormat, TiffCompression
    • ImagingHelpers, SKBitmapExtensions, DrawingInteropExtensions
  • DHI.Services.Rasters.Radar
    • IRadarImage, BaseRadarImage, RadarImageService<T>, ColorGradientType
    • PixelValueType, ConversionCoefficients, Funcs, BiasCorrectionService
    • Repositories: Radar.ESRIASCII.EsriAsciiRepository, Radar.DELIMITEDASCII.DelimitedAsciiRepository
    • Readers: Radar.IRISCAPPI.RadarImage, Radar.X00.RadarImage
  • DHI.Services.Rasters.Zones
    • Zone, PixelWeight, Weight, ZoneType
    • ZoneRepository, ZoneService
    • Converters: ZoneConverter, ZoneDictionaryConverter, ZoneTypeConverter
  • Connections
    • RasterServiceConnection<TRaster>, RadarImageServiceConnection<TImage>

Troubleshooting

  • “Folder contains no matching radar files” Check the file extension in filePattern and that the folder has files with that extension. The repository filters using that extension.
  • “Bad connection string format …” The repository connection string must be: folder;pattern;datetimeFormat (no extra semicolons except the two separators).
  • “The accumulated pixel weight is X but must be 1” Adjust your Zone.PixelWeights to sum to \~1.0.
  • “No rasters after/before …” Your requested time is outside the range. Use FirstDateTime() / LastDateTime() to bound queries.
  • TIFF export fails Ensure Magick.NET native dependencies are present. If you cannot install them in your runtime, export PNG/JPEG/WEBP instead.