diff --git a/notebooks/preprocessing-workflow/tiling.ipynb b/notebooks/preprocessing-workflow/tiling.ipynb new file mode 100644 index 00000000..c2983a1d --- /dev/null +++ b/notebooks/preprocessing-workflow/tiling.ipynb @@ -0,0 +1,286 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tiling Utilities" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook contains a demo of the tiling utilities included in `IceFloeTracker.jl`. In particular, the following workflows are illustrated:\n", + "\n", + "- Getting tiling with tiles of a given size\n", + "- Getting the optimal tile size given an initial tile side length\n", + "\n", + "Run the cell below to set up the computation environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "HOME = \"../..\" # path to the root of the project two levels up\n", + "\n", + "# Activate the environment\n", + "using Pkg\n", + "Pkg.activate(HOME)\n", + "Pkg.precompile()\n", + "\n", + "using IceFloeTracker: get_tiles, get_tile_dims, load\n", + "using ColorTypes: RGBA" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Load the image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "imgpath = \"test/test_inputs/NE_Greenland_truecolor.2020162.aqua.250m.tiff\"\n", + "img = load(joinpath(HOME,imgpath))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Estimate tile size\n", + "\n", + "Say we want to split the image into roughly 8x8 tiles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "prelim_sizes = size(img) .÷ 8" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Choosing tile size and building tilings\n", + "\n", + "The `get_tiles` function builds a tiling given an image (array) and a tile side length.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Let's view the documentation for the function\n", + "@info @doc get_tiles" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tiles = get_tiles(img, prelim_sizes[1] + 1) # deliberately using a size that is too large" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Top left tile dimensions\n", + "get_tile_dims(tiles[1])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the bottom right tile has been extended to cover the rest of the image as a uniform tiling of side length `prelim_sizes[1] + 1` fails to cover the full image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# View the first tile\n", + "img[tiles[1]...]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Further, note that the tiles on the right and bottom edges are slightly bigger." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Bottom right tile dimensions\n", + "get_tile_dims(tiles[end])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### View the full tiling" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "function build_tiling_illustration(img, side_length)\n", + "\n", + " # Create new canvas to draw on\n", + " newimg = similar(img, RGBA{Float64})\n", + "\n", + " # Apply transparency to the tiles\n", + " for tile_coords in get_tiles(img, side_length)\n", + " tile = @view img[tile_coords...]\n", + " alpha = rand(0.5:0.05:1)\n", + " transparent_tile = map(c -> RGBA(c.r, c.g, c.b, alpha), tile)\n", + " newimg[tile_coords...] .= transparent_tile\n", + " end\n", + "\n", + " # View the image\n", + " newimg\n", + "end\n", + "\n", + "build_tiling_illustration(img, prelim_sizes[1] + 1)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Optimal tile side length\n", + "\n", + "Perhaps there is a fitter tiling that is close to the originally desired tiling. We can use the `get_optimal_tile_size` function to determine whether this is possible." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "using IceFloeTracker: get_optimal_tile_size\n", + "\n", + "@info @doc get_optimal_tile_size" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "best_side_length = get_optimal_tile_size(prelim_sizes[1] + 1, size(img))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected, a tile size of 1016 is not optimal for this image. The function `get_optimal_tile_size` suggests a fitter tiling is possible using tiles of 1015 pixels in side length for this image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "build_tiling_illustration(img, best_side_length)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Area of corner tile with optimal side length\n", + "get_tile_dims(get_tiles(img, best_side_length)[end]) |> prod" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the area of the corner tile for the suboptimal tiling is larger." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "get_tile_dims(get_tiles(img, prelim_sizes[1] + 1)[end]) |> prod" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The proportion of pixels missed by a uniform tiling can be computed with `get_area_missed`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "using IceFloeTracker: get_area_missed\n", + "areamissed_optimal = get_area_missed(best_side_length, size(img))\n", + "@show areamissed_optimal\n", + "@assert get_area_missed(prelim_sizes[1] + 1, size(img)) > get_area_missed(best_side_length, size(img))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.9.3", + "language": "julia", + "name": "julia-1.9" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/tilingutils.jl b/src/tilingutils.jl index babcd993..1df1ceff 100644 --- a/src/tilingutils.jl +++ b/src/tilingutils.jl @@ -1,18 +1,62 @@ + +""" + getfit(dims::Tuple{Int,Int}, side_length::Int)::Tuple{Int,Int} + +Calculate how many tiles of a given side length fit into the given dimensions. + +# Arguments +- `dims::Tuple{Int,Int}`: A tuple representing the dimensions (width, height). +- `side_length::Int`: The side length of the tile. + +# Returns +- `Tuple{Int,Int}`: A tuple representing the number of tiles that fit along each dimension. + +# Examples +``` +julia> getfit((10, 20), 5) +(2, 4) + +julia> getfit((15, 25), 5) +(3, 5) +""" function getfit(dims::Tuple{Int,Int}, side_length::Int)::Tuple{Int,Int} return dims .÷ side_length end -function get_area_missed(side_length::Int, dims::Tuple{Int,Int}, area::Int)::Float64 + +""" + get_area_missed(side_length::Int, dims::Tuple{Int,Int})::Float64 + +Calculate the proportion of the area that is not covered by tiles of a given side length. + +# Arguments +- `side_length::Int`: The side length of the tile. +- `dims::Tuple{Int,Int}`: A tuple representing the dimensions (width, height). + +# Returns +- `Float64`: The proportion of the area that is not covered by the tiles. + +# Examples +``` +julia> get_area_missed(5, (10, 20)) +0.0 + +julia> get_area_missed(7, (10, 20)) +0.51 +""" +function get_area_missed(side_length::Int, dims::Tuple{Int,Int})::Float64 + area = prod(dims) return 1 - prod(getfit(dims, side_length)) * side_length^2 / area end + """ get_optimal_tile_size(l0::Int, dims::Tuple{Int,Int}) -> Int Calculate the optimal tile size in the range [l0-1, l0+1] for the given size `l0` and image dimensions `dims`. # Description -This function computes the optimal tile size for tiling an area with given dimensions. It ensures that the initial tile size `l0` is at least 2 and not larger than any of the given dimensions. The function evaluates candidate tile sizes and selects the one that minimizes the area missed during tiling. In case of a tie, it prefers the larger tile size. +This function computes the optimal tile size for tiling an area with given dimensions. It ensures that the initial tile size `l0` is at least 2 and not larger than any of the given dimensions. The function evaluates candidate tile sizes and selects the one that minimizes the area missed by its corresponding tiling. In case of a tie, it prefers the larger tile size. # Example ``` @@ -24,13 +68,12 @@ function get_optimal_tile_size(l0::Int, dims::Tuple{Int,Int})::Int l0 < 2 && error("l0 must be at least 2") any(l0 .> dims) && error("l0 = $l0 is too large for the given dimensions $dims") - area = prod(dims) minimal_shift = l0 == 2 ? 0 : 1 candidates = [l0 + i for i in -minimal_shift:1] minl, M = 0, Inf for side_length in candidates - missedarea = get_area_missed(side_length, dims, area) + missedarea = get_area_missed(side_length, dims) if missedarea <= M # prefer larger side_length in case of tie M, minl = missedarea, side_length end @@ -59,8 +102,24 @@ function get_tile_meta(tile) return [a, b, c, d] end +""" + bump_tile(tile::Tuple{UnitRange{Int64}, UnitRange{Int64}}, dims::Tuple{Int,Int})::Tuple{UnitRange{Int}, UnitRange{Int}} + +Adjust the tile dimensions by adding extra rows and columns. + +# Arguments +- `tile::Tuple{Int,Int,Int,Int}`: A tuple representing the tile dimensions (a, b, c, d). +- `dims::Tuple{Int,Int}`: A tuple representing the extra rows and columns to add (extrarows, extracols). + +# Returns +- `Tuple{UnitRange{Int}, UnitRange{Int}}`: A tuple of ranges representing the new tile dimensions. -function bump_tile(tile, dims) +# Examples +```julia +julia> bump_tile((1:3, 1:4), (1, 1)) +(1:4, 1:5) +""" +function bump_tile(tile::Tuple{UnitRange{S},UnitRange{S}}, dims::Tuple{S,S}) where {S<:Int} extrarows, extracols = dims a, b, c, d = get_tile_meta(tile) b += extrarows @@ -68,6 +127,22 @@ function bump_tile(tile, dims) return (a:b, c:d) end +""" + get_tile_dims(tile) + +Calculate the dimensions of a tile. + +# Arguments +- `tile::Tuple{UnitRange{Int},UnitRange{Int}}`: A tuple representing the tile dimensions. + +# Returns +- `Tuple{Int,Int}`: A tuple representing the width and height of the tile. + +# Examples +```julia +julia> get_tile_dims((1:3, 1:4)) +(4, 3) +""" function get_tile_dims(tile) a, b, c, d = get_tile_meta(tile) width, height = d - c + 1, b - a + 1