##############################################################################
# Copyright Kitware Inc.
#
# Licensed under the Apache License, Version 2.0 ( the "License" );
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##############################################################################
import colorsys
import itertools
import math
import re
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as _importlib_version
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from large_image.cache_util import LruCacheMetaclass, methodcache, strhash
from large_image.constants import TILE_FORMAT_NUMPY, TILE_FORMAT_PIL, SourcePriority
from large_image.exceptions import TileSourceError
from large_image.tilesource import TileSource
from large_image.tilesource.utilities import _imageToNumpy, _imageToPIL
try:
__version__ = _importlib_version(__name__)
except PackageNotFoundError:
# package is not installed
pass
_counters = {
'tiles': 0,
}
[docs]
class TestTileSource(TileSource, metaclass=LruCacheMetaclass):
cacheName = 'tilesource'
name = 'test'
extensions = {
None: SourcePriority.MANUAL,
}
def __init__(self, ignored_path=None, minLevel=0, maxLevel=9,
tileWidth=256, tileHeight=256, sizeX=None, sizeY=None,
fractal=False, frames=None, monochrome=False, bands=None,
**kwargs):
"""
Initialize the tile class. See the base class for other available
parameters.
:param ignored_path: for compatibility with FileTileSource.
:param minLevel: minimum tile level
:param maxLevel: maximum tile level. If both sizeX and sizeY are
specified, this value is ignored.
:param tileWidth: tile width in pixels
:param tileHeight: tile height in pixels
:param sizeX: image width in pixels at maximum level. Computed from
maxLevel and tileWidth if None.
:param sizeY: image height in pixels at maximum level. Computed from
maxLevel and tileHeight if None.
:param fractal: if True, and the tile size is square and a power of
two, draw a simple fractal on the tiles.
:param frames: if present, this is either a single number for generic
frames, a comma-separated list of c,z,t,xy, or a string of the
form '<axis>=<count>,<axis>=<count>,...'.
:param monochrome: if True, return single channel tiles.
:param bands: if present, a comma-separated list of band names.
Defaults to red,green,blue. Each band may optionally specify a
value range in the form "<band name>=<min val>-<max val>". If any
ranges are specified, bands with no ranges will use the union of
the specified ranges. The internal dtype with be uint8, uint16, or
float depending on the union of the specified ranges. If no ranges
are specified at all, it is the same as 0-255.
"""
if not kwargs.get('encoding'):
kwargs = kwargs.copy()
kwargs['encoding'] = 'PNG'
super().__init__(**kwargs)
self._spec = (
minLevel, maxLevel, tileWidth, tileHeight, sizeX, sizeY, fractal,
frames, monochrome, bands)
self.minLevel = minLevel
self.maxLevel = maxLevel
self.tileWidth = tileWidth
self.tileHeight = tileHeight
# Don't generate a fractal tile if the tile isn't square or not a power
# of 2 in size.
self.fractal = (fractal and self.tileWidth == self.tileHeight and
not (self.tileWidth & (self.tileWidth - 1)))
self.sizeX = (((2 ** self.maxLevel) * self.tileWidth)
if not sizeX else sizeX)
self.sizeY = (((2 ** self.maxLevel) * self.tileHeight)
if not sizeY else sizeY)
self.maxLevel = max(0, int(math.ceil(math.log2(max(
self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)))))
self.minLevel = min(self.minLevel, self.maxLevel)
self.monochrome = bool(monochrome)
self._bands = None
self._dtype = np.uint8
if bands:
bands = [re.match(
r'^(?P<key>[^=]+)(|=(?P<low>[+-]?((\d+(|\.\d*)))|(\.\d+))-(?P<high>[+-]?((\d+(|\.\d*))|(\.\d+))))$', # noqa
band) for band in bands.split(',')]
lows = [float(band.group('low'))
if band.group('low') is not None else None for band in bands]
highs = [float(band.group('high'))
if band.group('high') is not None else None for band in bands]
try:
low = min(v for v in lows + highs if v is not None)
high = max(v for v in lows + highs if v is not None)
except ValueError:
low = 0
high = 255
self._bands = {
band.group('key'): {
'low': lows[idx] if lows[idx] is not None else low,
'high': highs[idx] if highs[idx] is not None else high,
}
for idx, band in enumerate(bands)}
if low < 0 or high < 2 or low >= 65536 or high >= 65536:
self._dtype = np.dtype(float)
elif low >= 256 or high >= 256:
self._dtype = np.uint16
# Used for reporting tile information
self.levels = self.maxLevel + 1
if frames:
frameList = []
if '=' not in str(frames) and ',' not in str(frames):
self._axes = [('f', 'Index', int(frames))]
elif '=' not in str(frames):
self._axes = [
(axis, f'Index{axis.upper()}', int(part))
for axis, part in zip(['c', 'z', 't', 'xy'], frames.split(','))]
else:
self._axes = [
(part.split('=', 1)[0],
f'Index{part.split("=", 1)[0].upper()}',
int(part.split('=', 1)[1])) for part in frames.split(',')]
self._framesParts = len(self._axes)
axes = self._axes[::-1]
for fidx in itertools.product(*(range(part[-1]) for part in axes)):
curframe = {}
for idx in range(len(fidx)):
k = axes[idx][1]
v = fidx[idx]
if axes[idx][-1] > 1:
curframe[k] = v
frameList.append(curframe)
if len(frameList) > 1:
self._frames = frameList
[docs]
@classmethod
def canRead(cls, *args, **kwargs):
return True
[docs]
def fractalTile(self, image, x, y, widthCount, color=(0, 0, 0)):
"""
Draw a simple fractal in a tile image.
:param image: a Pil image to draw on. Modified.
:param x: the tile x position
:param y: the tile y position
:param widthCount: 2 ** z; the number of tiles across for a "full size"
image at this z level.
:param color: an rgb tuple on a scale of [0-255].
"""
imageDraw = ImageDraw.Draw(image)
x *= self.tileWidth
y *= self.tileHeight
sq = widthCount * self.tileWidth
while sq >= 4:
sq1 = sq // 4
sq2 = sq1 + sq // 2
for t in range(-(y % sq), self.tileWidth, sq):
if t + sq1 < self.tileWidth and t + sq2 >= 0:
for l in range(-(x % sq), self.tileWidth, sq):
if l + sq1 < self.tileWidth and l + sq2 >= 0:
imageDraw.rectangle([
max(-1, l + sq1), max(-1, t + sq1),
min(self.tileWidth, l + sq2 - 1),
min(self.tileWidth, t + sq2 - 1),
], color, None)
sq //= 2
def _tileImage(self, rgbColor, x, y, z, frame, band=None, bandnum=0):
image = Image.new(
mode='RGB',
size=(self.tileWidth, self.tileHeight),
color=(rgbColor if not self.fractal else (255, 255, 255)),
)
if self.fractal:
self.fractalTile(image, x, y, 2 ** z, rgbColor)
bandtext = '\n' if band is not None else ''
if bandnum and band and band.lower() not in {
'r', 'red', 'g', 'green', 'b', 'blue', 'grey', 'gray', 'alpha'}:
bandtext += band
image = _imageToNumpy(image)[0].astype(float)
vstripe = np.array([
int(x / (self.tileWidth / bandnum / 2)) % 2
for x in range(self.tileWidth)])
hstripe = np.array([
int(y / (self.tileHeight / (bandnum % self.tileWidth) / 2)) % 2
if bandnum > self.tileWidth else 1 for y in range(self.tileHeight)])
simage = image.copy()
simage[hstripe == 0, :, :] /= 2
simage[:, vstripe == 0, :] /= 2
image = np.where(image != 255, simage, image)
image = image.astype(np.uint8)
image = _imageToPIL(image)
imageDraw = ImageDraw.Draw(image)
fontsize = 0.15
text = 'x=%d\ny=%d\nz=%d' % (x, y, z)
if hasattr(self, '_frames'):
for k1, k2, _ in self._axes:
if k2 in self._frames[frame]:
text += '\n%s=%d' % (k1.upper(), self._frames[frame][k2])
text += bandtext
fontsize = min(fontsize, 0.8 / len(text.split('\n')))
try:
# the font size should fill the whole tile
imageDrawFont = ImageFont.truetype(
font='/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf',
size=int(fontsize * min(self.tileWidth, self.tileHeight)),
)
except OSError:
imageDrawFont = ImageFont.load_default()
imageDraw.multiline_text(
xy=(10, 10),
text=text,
fill=(0, 0, 0) if band != 'alpha' else (255, 255, 255),
font=imageDrawFont,
)
return image
[docs]
@methodcache()
def getTile(self, x, y, z, *args, **kwargs):
frame = self._getFrame(**kwargs)
self._xyzInRange(x, y, z, frame, len(self._frames) if hasattr(self, '_frames') else None)
if not (self.minLevel <= z <= self.maxLevel):
msg = 'z layer does not exist'
raise TileSourceError(msg)
_counters['tiles'] += 1
xFraction = (x + 0.5) * self.tileWidth * 2 ** (self.levels - 1 - z) / self.sizeX
yFraction = (y + 0.5) * self.tileHeight * 2 ** (self.levels - 1 - z) / self.sizeY
fFraction = yFraction
if hasattr(self, '_frames'):
fFraction = float(frame) / (len(self._frames) - 1)
backgroundColor = colorsys.hsv_to_rgb(
h=xFraction,
s=(0.3 + (0.7 * fFraction)),
v=(0.3 + (0.7 * yFraction)),
)
rgbColor = tuple(int(val * 255) for val in backgroundColor)
if not self._bands or len(self._bands) == (1 if self.monochrome else 3):
image = self._tileImage(rgbColor, x, y, z, frame)
if self.monochrome:
image = image.convert('L')
format = TILE_FORMAT_PIL
else:
image = np.zeros(
(self.tileHeight, self.tileWidth, len(self._bands)), dtype=self._dtype)
for bandnum, band in enumerate(self._bands):
bandimg = self._tileImage(rgbColor, x, y, z, frame, band, bandnum)
if self.monochrome or band.upper() in {'grey', 'gray', 'alpha'}:
bandimg = bandimg.convert('L')
bandimg = _imageToNumpy(bandimg)[0]
if (self._dtype != np.uint8 or
self._bands[band]['low'] != 0 or
self._bands[band]['high'] != 255):
bandimg = bandimg.astype(float)
bandimg = (bandimg / 255) * (
self._bands[band]['high'] - self._bands[band]['low']
) + self._bands[band]['low']
bandimg = bandimg.astype(self._dtype)
image[:, :, bandnum] = bandimg[:, :, bandnum % bandimg.shape[2]]
format = TILE_FORMAT_NUMPY
return self._outputTile(image, format, x, y, z, **kwargs)
[docs]
@staticmethod
def getLRUHash(*args, **kwargs):
return strhash(
super(TestTileSource, TestTileSource).getLRUHash(
*args, **kwargs),
kwargs.get('minLevel'), kwargs.get('maxLevel'),
kwargs.get('tileWidth'), kwargs.get('tileHeight'),
kwargs.get('fractal'), kwargs.get('sizeX'), kwargs.get('sizeY'),
kwargs.get('frames'), kwargs.get('monochrome'),
kwargs.get('bands'),
)
[docs]
def getState(self):
return 'test %r %r' % (super().getState(), self._spec)
[docs]
def open(*args, **kwargs):
"""
Create an instance of the module class.
"""
return TestTileSource(*args, **kwargs)
[docs]
def canRead(*args, **kwargs):
"""
Check if an input can be read by the module class.
"""
return TestTileSource.canRead(*args, **kwargs)