Table of Contents

Camera RAW

Camera RAW files hold the sensor data a camera recorded, before any processing. Egami reads DNG (.dng), the open RAW format Adobe publishes, which phones, Leica, Pentax, Ricoh, Hasselblad and Adobe DNG Converter write, and the makers' own formats: Canon CR2 and CR3, Nikon NEF and NRW, Pentax PEF, Sony ARW and SR2, Olympus ORF, Panasonic RW2 and RAW, Leica RWL, Fujifilm RAF and Samsung SRW. It develops them into an image on load (Canon CR3 and Samsung SRW have what is particular to those). Developing camera RAW is about choosing the develop. It reads through the Egami.Formats.Extended package: register it with WithRaw() or WithExtendedFormats() (Extended formats). RAW is detected ahead of TIFF, since a DNG file (like most maker formats) is a TIFF file; without it, a DNG opens as TIFF and only its preview pages load.

Reads DNG 1.0 to 1.7: colour filter arrays of any pattern and LinearRaw, uncompressed 1 to 32-bit integers and 16, 24 or 32-bit floats, lossless JPEG, Deflate with the integer and floating-point predictors, lossy JPEG and JPEG XL, in strips or tiles; linearization tables, black level patterns and deltas, white levels, active area and default crop; opcode lists 1 to 3; camera profiles (dual-illuminant colour matrices, ForwardMatrix, HueSatMap, LookTable, tone curve); embedded previews. The maker formats of Maker formats
Writes DNG: any RAW file Egami reads, with its sensor data unchanged (RawFormat.ConvertToDng), and any raster (Save with RawFormat.Info): a RAW mosaic as a colour filter array DNG, anything else as LinearRaw
Natural pixel type Rgb16 in sRGB for a develop, or in the RGB space ConvertColorTo names; for RawOutput.Mosaic, Gray16 (CFA) or Rgb16 (LinearRaw), GrayF32 or RgbF32 for float data; for a preview, the JPEG's Rgb8
FormatInfo RawFormat.Info: name RAW, extensions .dng, .cr2, .nef, .nrw, .pef, .arw, .sr2, .orf, .rw2, .rwl, .raf, .cr3 and .srw, with each format's MIME type. Panasonic's older .raw files are found by their header, not their extension
// RAW goes ahead of TIFF, since a DNG is a TIFF file; WithExtendedFormats() registers it too.
EgamiContext context = EgamiContext.Default.WithRaw();

// Load develops the sensor data: the camera's white balance and colour, AHD demosaicing, sRGB in Rgb16.
using Raster photo = Raster.Load("photo.dng", context);
photo.Save("photo.png");

// Probe reads the structure alone: the developed size, the camera and the embedded previews.
RasterInfo info = Raster.Probe("photo.dng", context);
Console.WriteLine($"{info.Width}×{info.Height}, {info.ImageCount} image(s)");

What Load returns

RawReaderOptions.Output (a RawOutput) picks it:

  • Developed (the default) runs the whole develop below.
  • HalfSize develops at half the width and height: each 2 × 2 block of the mosaic is one pixel, so there is no demosaicing and the develop is about four times faster.
  • Preview returns an embedded preview as the camera or converter rendered it, the largest unless PreviewIndex picks another. A file without one gives a half-size develop.
  • Mosaic returns the sensor data as stored, nothing scaled, cropped or balanced, for a pipeline of your own; the pattern, the black and white levels and the rest are in RawDetails.

With ReaderOptions.MinimumSize (and no Region), Load returns the smallest preview that covers the size, else a half-size develop if that does, else the full develop. LoadAll gives the developed image, then each preview.

// The untouched sensor data for a pipeline of your own: Gray16, one value per photosite.
using Raster mosaic = Raster.Load("photo.dng", context, new RawReaderOptions { Output = RawOutput.Mosaic });
RawDetails raw = (RawDetails)mosaic.Metadata.FormatDetails!;
Console.WriteLine($"{raw.CfaPattern}, black {raw.BlackLevels[0]}, white {raw.WhiteLevels[0]}");

// Half the size and about four times faster: each 2 × 2 block of photosites is one pixel.
using Raster<Rgba8> half = Raster.Load<Rgba8>("photo.dng", context, new RawReaderOptions { Output = RawOutput.HalfSize });

// A thumbnail: MinimumSize takes the smallest embedded preview or half-size develop that covers it.
using Raster thumb = Raster.Load("photo.dng", context, new ReaderOptions { MinimumSize = new Size(32, 32) });

Maker formats

A maker file goes through the same develop as a DNG. What a DNG carries in its tags comes from elsewhere:

  • The sensor data is decoded by each maker's codec: Canon's sliced lossless JPEG and sRAW and mRAW, Nikon's Huffman coding and curve, Pentax's Huffman coding, Sony's ARW2, ARW1 and lossless JPEG tiles, Olympus's and Panasonic's own codings (Panasonic RawFormat 4 to 8, and the 12-bit packed and 16-bit data of older files without a RawFormat tag, such as the DMC-FZ8, FZ50 and LX1 and the Leica Digilux 3), Fujifilm's compressed codec (lossless and lossy, X-Trans and Bayer), Samsung's three SRW codecs and the plain packings. Decoded mosaics are bit for bit those Adobe DNG Converter stores (on 41 CC0 sample files), but for defective pixels and clipping.
  • White balance, black levels and crops come from the file and its maker note where it has them. The active area is the sensor's own (from the maker's sensor border tags, or the one Adobe gives the camera), with the default crop inside it, so a develop has the pixels around the crop to work with.
  • Colour matrices, and levels and crops a file does not give, come from Egami's camera table: for each camera, what Adobe DNG Converter writes in a DNG of a sample file, measured as a black box. A camera that is not in the table develops with a generic matrix and a CameraUnknown warning.
  • Canon sRAW and mRAW files are LinearRaw (three samples a pixel), so RawOutput.Mosaic gives Rgb16. Fujifilm X-Trans sensors (6 × 6 pattern) are demosaiced by Egami's own gradient-weighted method whatever Demosaic says. A RAF's dynamic-range setting moves the baseline exposure as it does in Adobe's DNGs.

Not read: Sony SRF, Nikon's small (sNEF) and High Efficiency raws, the Sony A100, RAFs from before 2012 whose CFA section is not a TIFF, and 14-bit packed RAFs. They throw NotSupportedException.

Canon CR3

Canon's CR3 files (EOS R and M bodies, the 250D, 850D and 90D, the 1D X Mark III and recent PowerShots) are an ISO base media file. Egami reads their three previews, all JPEGs: the full-size one, a 1620 × 1080 (or 1440 × 1080) one and a 160 × 120 thumbnail; and their metadata: EXIF, GPS, and Canon's maker note as bytes in RawDetails.MakerNote. Raster.Probe gives the sensor data's size, and RawOutput.Preview (with PreviewIndex, or ReaderOptions.MinimumSize) loads a preview.

The sensor data is coded with Canon's CRX codec, which Egami decodes in both its forms: lossless, and C-RAW (a quantised 5/3 wavelet). The mosaic matches Adobe DNG Converter's bit for bit inside the active area, and the black and white levels, the as-shot white balance, the active area and the crop are read from the file, so a CR3 develops, loads as a mosaic and converts to DNG like the other maker formats. A Dual Pixel RAW develops its main image, and a RAW burst (roll) file its first frame. CRX data of encoding type 1 and the 8-bit single-plane form, which no camera is known to write, throw NotSupportedException; their previews still load. The tiles and planes decode in parallel.

Samsung SRW

Samsung's SRW files (the NX system cameras from the NX5 and NX10 to the NX1 and NX500, the Galaxy NX, and the EX1, EX2F and WB2000 compacts) are TIFF files whose raw IFD's compression names the layout: 12-bit packed or 16-bit uncompressed data (the NX5 to NX1100 generation, the EX1 and WB2000), or one of Samsung's three codecs: V0 (the NX300, NX2000, NX30 and Galaxy NX), V1 (the NX mini, NX3000 and NX3300) and V2 (the NX1 and NX500, 12 or 14-bit). Over 22 CC0 sample files, one per body and mode, the mosaic matches Adobe DNG Converter's bit for bit inside the active area. The black levels and the as-shot white balance come from the maker note, the white level, active area, crop and colour matrices from the camera table. 12-bit packed data under compression 32769, which no sample is known to have, throws NotSupportedException. The three codecs predict from the rows above, so they decode on one thread. The Pentax-made GX-10, GX-20 and GX-1 (make SAMSUNG TECHWIN) are PEF files.

The develop

The develop follows the DNG specification's processing order:

  1. Opcode list 1 on the stored values; then the linearization table, the black levels (per position of the black level pattern, plus the per-row and per-column deltas) and the white level map the active area to 0 (black) to 1 (clipping).
  2. Opcode list 2 (gain maps for lens shading, polynomial and table maps, bad-pixel fixes).
  3. Luminance noise reduction, when LuminanceNoiseReduction (0 to 100) is above 0: wavelet shrinkage of each CFA position's samples, with the file's NoiseProfile as the noise model, or one measured from the image.
  4. White balance (RawWhiteBalance): as shot (the camera's AsShotNeutral or AsShotWhiteXY), automatic (the image's average as gray), or custom (Temperature in kelvin and Tint). The colour matrices of the two calibration illuminants are interpolated for the white's temperature.
  5. Demosaicing (RawDemosaicMethod): Bilinear, Malvar (Malvar, He and Cutler's gradient-corrected interpolation) or Ahd (Hirakawa and Parks' adaptive homogeneity-directed method, the default). A pattern other than a 2 × 2 Bayer one gets a same-colour neighbourhood average.
  6. Opcode list 3 (lens distortion through WarpRectilinear and WarpFisheye, radial vignetting); LensCorrections = false skips the lens opcodes.
  7. Highlights (RawHighlightMode): Clip cuts every colour where the least amplified one clips, so clipped areas turn white; Blend lifts what clipping removed evenly across the colours; Rebuild estimates clipped colours from unclipped ones with the colour of the surroundings, which brings detail back when Exposure is lowered.
  8. Colour noise reduction (ColorNoiseReduction, 0 to 100, default 25): wavelet shrinkage of two colour-difference planes, leaving luminance alone.
  9. The default crop; camera RGB to XYZ D50 through the ForwardMatrix, or the inverted ColorMatrix and Bradford adaptation; the camera profile's HueSatMap, then exposure (Exposure plus the file's BaselineExposure unless ApplyBaselineExposure is off), then its LookTable and tone curve, all in linear ProPhoto RGB (ApplyProfile = false leaves them out); and the output colour space. A profile without a tone curve (every maker file's) leaves the develop scene-linear; DefaultToneCurve = true gives it a contrast curve measured from Adobe Camera Raw's default rendering instead (tools/measure-raw-tone-curve.py), which lifts the midtones (18 % gray comes out at about 38 %) and rolls off the highlights.

The output is sRGB unless ReaderOptions.ConvertColorTo names another RGB space (Display P3, Adobe RGB, ProPhoto, Rec. 2020 or a linear one): the develop renders straight into it, so wide-gamut colours are never clipped to sRGB first, and the raster carries its ICC profile. Other colour models (CMYK, gray) are converted to after the develop. Orientation is not applied: RasterInfo.Orientation and the EXIF Orientation say how to turn the image, and AutoOrient does it.

RawReaderOptions develop = new()
{
    WhiteBalance = RawWhiteBalance.Custom,
    Temperature = 4800,                    // kelvin
    Tint = 5,                              // toward magenta
    Exposure = -0.5,                       // stops
    Highlights = RawHighlightMode.Rebuild, // recover clipped colour from what was not clipped
    Demosaic = RawDemosaicMethod.Malvar,   // faster than the default AHD
    LuminanceNoiseReduction = 20,          // 0 to 100; off by default
    ColorNoiseReduction = 40,              // 0 to 100; 25 by default
    AutoChromaticAberration = true,        // fix colour fringes when no lens profile covers the file
    DefaultToneCurve = true,               // a contrast curve like Adobe Camera Raw's default; off by default
    ConvertColorTo = Colors.ColorProfile.DisplayP3, // rendered straight into P3, the profile attached
};
using Raster photo = Raster.Load("photo.dng", context, develop);
photo.Save("photo-p3.png");

The develop runs in parallel bands, and its output is the same for any EgamiContext.MaxParallelism.

Lens profiles

A file that carries its own lens corrections (a DNG's warp and vignetting opcodes) is corrected with those. For other files, RawReaderOptions.LensProfiles names folders of lens profiles that Egami reads at run time: Lensfun database files (.xml) and Adobe lens profiles (.lcp). Two kinds of folders are searched as well when they are there: a system Lensfun install (such as /usr/share/lensfun on Linux), and the folders Adobe's software (Camera Raw, Lightroom, DNG Converter) installs its lens profiles in, %ProgramData%\Adobe\CameraRaw\LensProfiles\1.0 and %AppData%\Adobe\CameraRaw\LensProfiles\1.0 on Windows and /Library/Application Support/Adobe/CameraRaw/LensProfiles/1.0 and its counterpart in your own Library on macOS. Egami only reads them. The profile whose lens name matches the file's EXIF LensModel (an Adobe profile's name as people read it, or as cameras write it) corrects vignetting, distortion and lateral chromatic aberration, its calibrations interpolated for the shot's focal length and aperture, at the calibrated focus distance nearest the shot's; RawDetails.LensProfile says which one did. The corrections measure from the output image (the default crop), the image the profiles were made for.

Adobe's folder holds thousands of profiles, so Egami reads only those whose file name has the lens's focal length, the first time a lens is developed (about two seconds for a common zoom), and keeps them for the process.

Egami ships no profiles. Lensfun's database is under CC BY-SA 3.0 and Adobe's profiles come with Adobe's software, so you supply them, or install that software, and their terms apply to them. LensCorrections = false turns every lens correction off.

For a file no profile covers, AutoChromaticAberration = true measures lateral chromatic aberration from the image itself and scales red and blue about the centre to line their edges up with green's. It is off by default: the measurement costs about as much as the demosaic.

Details and metadata

RawDetails, in RasterMetadata.FormatDetails, says what the raster is (Output, PreviewIndex), the camera (Make, Model, UniqueCameraModel, DngVersion), the sensor data (SensorWidth, SensorHeight, ActiveArea, CropArea, CfaPattern, BitsPerSample, Compression, BlackLevels, WhiteLevels), the colour the develop used (AsShotNeutral, WhiteBalance, Temperature, Tint, CameraToXyz, ProfileName, BaselineExposure), the Opcodes, the Previews (each a RawPreview of a RawPreviewKind), the exposure settings and lens, and the maker note as bytes.

EXIF (with GPS) and XMP come through as for TIFF.

using Raster photo = Raster.Load("photo.dng", context);
RawDetails details = (RawDetails)photo.Metadata.FormatDetails!;
Console.WriteLine($"{details.Make} {details.Model}, DNG {details.DngVersion}");
Console.WriteLine($"Sensor {details.SensorWidth}×{details.SensorHeight}, crop {details.CropArea}");
Console.WriteLine($"White balance {details.Temperature:F0} K, tint {details.Tint:F0}, multipliers {string.Join(", ", details.WhiteBalance)}");
foreach (RawPreview preview in details.Previews)
{
    Console.WriteLine($"Preview {preview.Width}×{preview.Height} {preview.Kind}");
}

Limits

  • The maker formats above.
  • Fujifilm GFX 100 files (100 MP) need MaxTotalBytes above its 2 GB default for a full develop; their previews, half-size develop and mosaic fit.
  • Three-colour data only: monochrome DNGs and four-colour (CMYG) sensors are not developed (their mosaic loads).
  • Non-square pixels (DefaultScale other than 1) are developed square. The third calibration illuminant of DNG 1.6, profile gain table maps, semantic masks and extra camera profiles are not read; opcode TrimBounds and WarpRectilinear2 are skipped with a warning.
  • Lens corrections that cameras record in their own maker notes (Fujifilm, Micro Four Thirds, Nikon Z, Sony) are not read; a lens profile, or AutoChromaticAberration, corrects those files instead.
  • Without a NoiseProfile (maker formats), luminance noise reduction measures the noise from the flattest parts of the image. Over the corpus the measure is a median 1.2 times Adobe's calibrated profiles, but for a few files (strongly textured scenes, the Canon EOS 350D, the Panasonic GH6) it reads up to several times higher, so the same setting reduces more. Luminance noise reduction works on the mosaic, so Canon sRAW and mRAW files get colour noise reduction only.
  • Adobe DNG Converter picks a Canon sRAW or mRAW file's white level by its ISO, from its own tables, and moves the baseline exposure with it. Egami uses the pair Adobe gave the camera's sample, which develops to the same brightness at every ISO, but clips highlights at that sample's white level: at other ISOs Adobe's can be higher (EOS 7D: 9000 at ISO 160 and 1250, 11400 at most others) or lower (EOS 6D and 70D: 11800 at ISO 160 and 1250, against 13000).
  • Adobe DNG Converter stores the Panasonic DMC-L10's values with its black level (15) subtracted; Egami keeps the file's values and gives the black level instead, which develops the same.
  • DNG writing writes BigTIFF past 4 GB (which DNG readers, Egami's included, may not open), and makes previews from Egami's own develop.

Writing DNG

// Any RAW file Egami reads becomes a DNG with its sensor data unchanged: levels, crop, colour data and EXIF come along.
RawFormat.ConvertToDng("photo.dng", "copy.dng", new DngWriterOptions { Preview = DngPreview.Thumbnail }, context);

// A transcode with no operations takes the same route; with operations, the develop is saved as a linear DNG.
Raster.Transcode("photo.dng", "same.dng", context: context);
Raster.Transcode("photo.dng", "small.dng", ops => ops.Fit(800, 800), context: context);

RawFormat.ConvertToDng copies the sensor data as Egami decodes it: not demosaiced, scaled or cropped. The file is laid out as Adobe DNG Converter writes it: a 256-pixel thumbnail and the camera tags in IFD0, the raw data in tiles in a SubIFD, and a JPEG preview in another. A converted file keeps the source's levels, active area, crop, colour filter pattern, linearization table and opcode lists, a colour profile from the file or the camera table (both illuminants' ColorMatrix, CameraCalibration and ForwardMatrix, and the baseline exposure), EXIF, XMP and the maker note, which goes into DNGPrivateData in Adobe's form. A lossy Nikon NEF is stored as Adobe stores it: the codes before the camera's curve, with the curve as the LinearizationTable, when the curve rises over the codes the file uses (the codes span a few hundred values, so they compress much better). A DNG source keeps every tag it had, its values exactly. A camera with colour data in neither the file nor the table gets the profile Egami develops it with (the camera treated as sRGB) and a Raw.DngCameraUnknown warning through EgamiContext.Diagnostics.

Raster.Transcode and egami convert take the same route when the output is .dng and there are no operations: the RAW format's writer is an IFormatConverter (Custom formats). With operations, the developed image is saved instead, as LinearRaw.

// A mosaic keeps its RawDetails, so it saves as a colour filter array DNG again, after work of your own on the photosites.
using Raster<Gray16> mosaic = Raster.Load<Gray16>("photo.dng", context, new RawReaderOptions { Output = RawOutput.Mosaic });
using (FileStream file = File.Create("mosaic.dng"))
{
    mosaic.Save(file, RawFormat.Info, new DngWriterOptions { Compression = DngCompression.Deflate });
}

// Any other raster saves as a linear DNG, which raw converters open as a finished photo they can still white-balance.
using Raster<Rgb16> developed = Raster.Load<Rgb16>("photo.dng", context);
using (FileStream file = File.Create("linear.dng"))
{
    developed.Save(file, RawFormat.Info, new DngWriterOptions { Preview = DngPreview.Thumbnail });
}

Save with RawFormat.Info writes a Gray16 or GrayF32 raster whose FormatDetails is a RAW mosaic's RawDetails as a colour filter array DNG, with the details' pattern, levels and areas and the camera table's profile (or, for a camera not in it, one worked out from CameraToXyz). Any other raster becomes LinearRaw: linear RGB in its own primaries (sRGB when it has no RGB profile), 16-bit or float, black 0, with a ColorMatrix and ForwardMatrix for D50 worked out from the primaries. Its previews show the raster itself.

DngWriterOptions Default Does
Compression DngCompression.LosslessJpeg Lossless JPEG, as Adobe writes, a Bayer row coded as pairs (other patterns as one component); floating-point data goes in Deflate instead. None, Deflate (with TIFF's integer or floating-point predictor), or DNG 1.7's lossless JpegXl
TileSize null Tiles near 256 pixels, sized along each axis to leave the least padding, as Adobe sizes them; or a square size, a multiple of 16
Preview DngPreview.Medium A 256-pixel thumbnail and a 1024-pixel JPEG; Thumbnail, Full (a full-size JPEG) or None (the raw data is the first image)
ParallelCompression true Codes tiles on several threads; the file is the same

On the 57 corpus files Egami reads as RAW, a converted DNG loads back with its mosaic (after its linearization table), levels, areas and pattern bit for bit, and develops to the source's develop within a level in 16 bits. Adobe DNG Converter, LibRaw (through Magick.NET), Windows' Raw Image Extension and Adobe's dng_validate open it. Its colour tags are Adobe's for the same file. With lossless JPEG and the medium preview it is 0.9 % smaller than Adobe's DNG over the corpus and within 2 % file by file. On one thread it converts the corpus in 70 % of the time Adobe DNG Converter takes pinned to one core, and each file faster than Adobe: the closest, a 16-bit Fujifilm GFX100S file, in 90 % of Adobe's time.

Limits: a DNG past 4 GB is written as BigTIFF, which Egami's own RAW reader does not open. Save has no context, so it reports no Raw.DngCameraUnknown warning; ConvertToDng and Transcode do. dng_validate warns of too little padding around the crop of Fujifilm files and the Leica C-Lux, as it does for Adobe's own DNGs of them, and of DNGs converted from phone DNGs, whose own areas leave none.