Skip to content

Commit

Permalink
16-bit
Browse files Browse the repository at this point in the history
  • Loading branch information
chaichontat committed Mar 8, 2024
1 parent c214df9 commit baa3df1
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 35 deletions.
49 changes: 33 additions & 16 deletions loopy/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class ImageParams(ReadonlyModel):
channels: list[str] | Literal["rgb"]
defaultChannels: dict[Colors, str] | None = None
mPerPx: float
dtype: Literal['uint8', 'uint16'] | None = None
maxVal: int | None = None

def write(self, f: Callable[[Self], None]) -> Self:
f(self)
Expand Down Expand Up @@ -65,6 +67,7 @@ def from_img(
scale: float,
translate: tuple[float, float] = (0, 0),
rgb: bool = False,
convert_to_8bit: bool = False,
) -> Self:
if rgb:
height, width, chans = img.shape
Expand All @@ -87,11 +90,12 @@ def from_img(
if img.dtype == np.uint8:
...
elif img.dtype == np.uint16:
log("Converting uint16 to uint8.", type_="WARNING")
dived = np.divide(img, 256, casting="unsafe") # So that this remains an uint16.
del img
img = dived.astype(np.uint8)
del dived
if convert_to_8bit:
log("Converting uint16 to uint8.", type_="WARNING")
dived = np.divide(img, 256, casting="unsafe") # So that this remains an uint16.
del img
img = dived.astype(np.uint8)
del dived
else:
raise ValueError(f"Unsupported dtype for TIFF file. Found {img.dtype}. Expected uint8 or uint16.")

Expand All @@ -107,7 +111,7 @@ def from_img(
)

def transform_tiff(
self, path_in: Path, quality: int = 90, logger: Callback = log, save_uncompressed: bool = False
self, path_in: Path, *, quality: int = 90, logger: Callback = log, save_uncompressed: bool = False
) -> tuple[list[str], Callable[[], None]]:
logger(f"Transforming {path_in} to COG.")
if path_in.suffix != ".tif":
Expand All @@ -117,16 +121,25 @@ def transform_tiff(
names = [path_in.stem + name + ".tif" for name in names]

def run():
if not names and not chanlist:
return

for name, c in zip(names, chanlist):
self._write_uncompressed_geotiff(
is_16bit = self._write_uncompressed_geotiff(
path=path_in.with_name(name),
channels=c,
transform=rasterio.Affine(
self.scale, 0, self.translate[0], 0, -self.scale, -self.translate[1]
),
)

self._compress([path_in.with_name(name) for name in names], quality=quality, logger=logger)
self._compress(
[path_in.with_name(name) for name in names],
is_16bit=is_16bit, # type: ignore
quality=quality,
logger=logger,
save_uncompressed=save_uncompressed
)

return names, run

Expand Down Expand Up @@ -165,6 +178,10 @@ def _write_uncompressed_geotiff(
dst: DatasetWriter
# Not compressing here since we cannot control the compression level.
assert 0 < len(channels) <= 3
dtype = self._get_slide(0).dtype
if dtype != np.uint8 and dtype != np.uint16:
raise ValueError(f"Unsupported dtype {dtype}. Expected uint8 or uint16.")

with rasterio.open(
path.with_suffix(".tif_").as_posix(),
"w",
Expand All @@ -174,7 +191,7 @@ def _write_uncompressed_geotiff(
count=len(channels),
photometric="RGB" if self.rgb else "MINISBLACK",
transform=transform, # https://gdal.org/tutorials/geotransforms_tut.html # Flip y-axis.
dtype=np.uint8,
dtype=dtype,
crs="EPSG:32648", # meters
tiled=True,
) as dst: # type: ignore
Expand All @@ -183,9 +200,10 @@ def _write_uncompressed_geotiff(
dst.write(self._get_slide(idx_in), idx_out)
dst.build_overviews([4, 8, 16, 32, 64], Resampling.nearest)
assert path.with_suffix(".tif_").exists()
return dtype == np.uint16

def _compress(
self, ps: list[Path], quality: int = 90, logger: Callback = log, save_uncompressed: bool = False
self, ps: list[Path], *, is_16bit: bool = False, quality: int = 90, logger: Callback = log, save_uncompressed: bool = False
) -> None:
def run(p: Path):
logger("Writing COG", p.with_suffix(".tif").as_posix())
Expand All @@ -194,6 +212,10 @@ def run(p: Path):
if getattr(sys, "frozen", False) and sys.platform != "win32"
else "gdal_translate"
)
if is_16bit and quality != 100:
logger("Quality is set to 100 (lossless) for 16-bit images.", type_="WARNING")

compression = ["-co", "COMPRESS=LERC_DEFLATE"] if is_16bit else ["-co", "COMPRESS=JPEG", "-co", f"JPEG_QUALITY={int(quality)}"]
# https://stackoverflow.com/questions/4417546/constantly-print-subprocess-output-while-process-is-running/4417735
with subprocess.Popen(
[
Expand All @@ -202,12 +224,7 @@ def run(p: Path):
p.with_suffix(".tif").as_posix(),
"-co",
"TILED=YES",
"-co",
"COMPRESS=JPEG",
"-co",
"COPY_SRC_OVERVIEWS=YES",
"-co",
f"JPEG_QUALITY={int(quality)}",
*compression,
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
Expand Down
3 changes: 3 additions & 0 deletions loopy/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pathlib import Path
from typing import Any, Callable, Concatenate, Generic, Literal, ParamSpec, Protocol, TypeVar

import numpy as np
import pandas as pd
from pydantic import BaseModel
from typing_extensions import Self
Expand Down Expand Up @@ -175,6 +176,8 @@ def add_image(
channels=channels,
mPerPx=geotiff.scale,
defaultChannels=defaultChannels,
dtype="uint8" if geotiff.img.dtype == np.uint8 else "uint16",
maxVal=geotiff.img.max()
)
return self

Expand Down
9 changes: 8 additions & 1 deletion src/lib/data/objects/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,24 @@ export type ImageParams = {
channels: string[] | 'rgb';
mPerPx: number;
defaultChannels?: Record<BandInfo['color'], string | undefined>;
dtype?: 'uint8' | 'uint16';
maxVal?: number;
};

export class ImgData extends Deferrable {
urls: readonly Url[];
channels: string[] | 'rgb';
defaultChannels: Record<BandInfo['color'], string | undefined>;
mPerPx: number;
maxVal: number;

constructor({ urls, channels, defaultChannels, mPerPx }: ImageParams, autoHydrate = false) {
constructor(
{ urls, channels, defaultChannels, mPerPx, maxVal }: ImageParams,
autoHydrate = false
) {
super();
this.urls = urls;
this.maxVal = maxVal ?? 255;

// WebGL limitation
if (Array.isArray(channels) && !channels.every((c) => /^[a-z0-9_]+$/i.test(c))) {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/ui/background/imgBackground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ export class Background extends Deferrable {
this.source.bandCount = this.image.mode === 'rgb' ? 3 : image.channels.length;

this.layer = new WebGLTileLayer({
style: Array.isArray(image.channels) ? genCompStyle(image.channels) : genRGBStyle(),
style: Array.isArray(image.channels)
? genCompStyle(image.channels, Math.round(image.maxVal / 2))
: genRGBStyle(),
source: this.source,
zIndex: -1
});
Expand Down
4 changes: 2 additions & 2 deletions src/lib/ui/background/imgColormap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ export type CompCtrl = { type: 'composite'; variables: Record<string, BandInfo>
export type RGBCtrl = { type: 'rgb'; Exposure: number; Contrast: number; Saturation: number };
export type ImgCtrl = CompCtrl | RGBCtrl;

export function genCompStyle(bands: string[]): Style {
export function genCompStyle(bands: string[], max: number): Style {
const vars: Record<string, number> = {};
bands.forEach((b, i) => {
vars[b] = i + 1;
vars[`${b}Max`] = 128;
vars[`${b}Max`] = max;
vars[`${b}Min`] = 0;
vars[`${b}redMask`] = 1;
vars[`${b}greenMask`] = 1;
Expand Down
23 changes: 12 additions & 11 deletions src/lib/ui/background/imgControl.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import { classes } from '$lib/utils';
import type { ImgData } from '$src/lib/data/objects/image';
import {
bgColors,
colors,
type BandInfo,
type CompCtrl,
type ImgCtrl
bgColors,
colors,
type BandInfo,
type CompCtrl,
type ImgCtrl
} from '$src/lib/ui/background/imgColormap';
import { isEqual, zip } from 'lodash-es';
import { onMount } from 'svelte';
Expand Down Expand Up @@ -41,19 +41,20 @@
}
}
const half = Math.round(image.maxVal / 2)
for (const [chan, color] of zip(image.channels, colors)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
bandinfo[chan!] = { enabled: false, color: color!, minmax: [0, 128] };
bandinfo[chan!] = { enabled: false, color: color!, minmax: [0, half] };
}
if (Object.keys(image.defaultChannels).length > 0) {
for (const [c, b] of Object.entries(image.defaultChannels)) {
if (b) bandinfo[b] = { enabled: true, color: c, minmax: [0, 128] };
if (b) bandinfo[b] = { enabled: true, color: c, minmax: [0, half] };
}
} else {
bandinfo[image.channels[0]] = { enabled: true, color: 'red', minmax: [0, 128] };
bandinfo[image.channels[1]] = { enabled: true, color: 'green', minmax: [0, 128] };
bandinfo[image.channels[2]] = { enabled: true, color: 'blue', minmax: [0, 128] };
bandinfo[image.channels[0]] = { enabled: true, color: 'red', minmax: [0, half] };
bandinfo[image.channels[1]] = { enabled: true, color: 'green', minmax: [0, half] };
bandinfo[image.channels[2]] = { enabled: true, color: 'blue', minmax: [0, half] };
}
imgCtrl = {
Expand Down Expand Up @@ -142,7 +143,7 @@
<div class="min-w-[128px] pl-0.5 cursor-pointer">
<RangeSlider
min={0}
max={255}
max={image.maxVal}
range
springValues={{ stiffness: 1, damping: 1 }}
on:start={() => handleClick(name, imgCtrl.variables[name].color)}
Expand Down
9 changes: 5 additions & 4 deletions src/lib/ui/webglcolor.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
export class WebGLColorFunc {
static subtract(a: unknown[], b: unknown[]) {
return ['-', a, b]
return ['-', a, b];
}
static normalize(band: string) {
return ['/',
this.clamp(this.subtract(['band', ['var', band]], ['var', `${band}Min`]), 0, 255),
this.clamp(this.subtract(['var', `${band}Max`], ['var', `${band}Min`]), 1, 255)
return [
'/',
this.clamp(this.subtract(['band', ['var', band]], ['var', `${band}Min`]), 0, 65535),
this.clamp(this.subtract(['var', `${band}Max`], ['var', `${band}Min`]), 1, 65535)
];
}

Expand Down

0 comments on commit baa3df1

Please sign in to comment.