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 aDateTimeid.- 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/UpperRightPixelSize(geo units per pixel)PixelValueUnit(e.g.,"mm/h")GetGeoCoordinates(Pixel)→ the pixel’s corner coordinatesToBitmap()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(extendsIRaster)PixelValueType(Reflectivity | Intensity)GetIntensity(Pixel)(converts reflectivity to intensity if needed)ToIntensity(...)to convert the whole image
BaseRadarImageimplements conversion defaults:- Marshall-Palmer conversion (A=200, B=1.6) by default
- Units default to mm/h (you can switch to µm/s)
PixelValueType– Reflectivity (dBZ) and Intensity (float)ConversionCoefficients– customize Reflectivity→Intensity conversionFuncs.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\radarfilePattern: E.g.Radar33{datetimeFormat}.asciidateTimeFormat: 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
- You can embed counters:
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 subclassingBaseRadarImageRepository<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¶
ZonePixelWeights : HashSet<PixelWeight>ImageSize : SKSizeIType : 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)
- Adds validation rules on
JSON (de)serialization¶
We ship custom converters:
ZoneConverterfor a single zoneZoneDictionaryConverterfor a dictionary of zonesZoneTypeConverterforZoneType
Metadata: zones/entities support a
Metadatadictionary. OurMetadataConverterreads 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>
- Spatially varying (Jensen, 2015) from
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
PixelValueTypetoReflectivity(code 2) orIntensity(code 13) - Fills geo center from radar lat/lon in product metadata
- Sets
PixelSizefrom product scales
LAWR X00 (Radar.X00.RadarImage)¶
- Reads DHI LAWR
.x00headers + payload Signaltype 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.IntensityDefaultColorGradientType.IntensityLightYellowToRedColorGradientType.IntensityLightYellowToRedLogarithmicColorGradientType.ReflectivityDefaultColorGradientType.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, orNone) - 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, passnew Pixel(col,row)starting at 1. - Zone weights must sum to \~1.0.
ZoneService.Addwill reject invalid zones. - Time windows too large.
RadarImageServicedefaults: analysis ≤ 30 days, batch fetch ≤ 1 day. Use the constructor overload to relax if needed. - NoData handling. Many readers set
Funcs.NoDatafor invalid/missing values; your stats should ignore it. - Reflectivity conversion range.
ReflectivityToIntensityenforces 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.
MetadataConverteris 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,TiffCompressionImagingHelpers,SKBitmapExtensions,DrawingInteropExtensions
- DHI.Services.Rasters.Radar
IRadarImage,BaseRadarImage,RadarImageService<T>,ColorGradientTypePixelValueType,ConversionCoefficients,Funcs,BiasCorrectionService- Repositories:
Radar.ESRIASCII.EsriAsciiRepository,Radar.DELIMITEDASCII.DelimitedAsciiRepository - Readers:
Radar.IRISCAPPI.RadarImage,Radar.X00.RadarImage
- DHI.Services.Rasters.Zones
Zone,PixelWeight,Weight,ZoneTypeZoneRepository,ZoneService- Converters:
ZoneConverter,ZoneDictionaryConverter,ZoneTypeConverter
- Connections
RasterServiceConnection<TRaster>,RadarImageServiceConnection<TImage>
Troubleshooting¶
- “Folder contains no matching radar files”
Check the file extension in
filePatternand 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.PixelWeightsto 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.NETnative dependencies are present. If you cannot install them in your runtime, export PNG/JPEG/WEBP instead.