Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"permissions": {
"allow": [
"Bash(julia --project=. -e 'using Pkg; Pkg.test\\(\\)')"
]
}
}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 -c \"import sys,json; d=json.load(sys.stdin); print(d['tool_input'].get('file_path',''))\" | { read -r f; [[ \"$f\" == *.jl ]] && runic -i \"$f\"; } 2>/dev/null || true",
"statusMessage": "Formatting Julia with Runic..."
}
]
}
]
}
}
2 changes: 2 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# runic formatting
fbb2336
8 changes: 4 additions & 4 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ jobs:
fail-fast: false
matrix:
version:
- '1.1'
- 'min'
- '1'
os:
- ubuntu-latest
arch:
- x64
steps:
- uses: actions/checkout@v2
- uses: julia-actions/setup-julia@v1
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
Expand All @@ -40,6 +40,6 @@ jobs:
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v1
- uses: codecov/codecov-action@v4
with:
file: lcov.info
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
*.jl.mem
deps/deps.jl
Manifest.toml
Manifest-v*.toml
13 changes: 10 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
name = "RegisterUtilities"
uuid = "d4862ba2-f42c-5aeb-af4f-96a8884a16c4"
version = "1.0.0"
authors = ["Tim Holy <tim.holy@gmail.com>"]
version = "0.1.0"

[deps]
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
RegisterCore = "67712758-55e7-5c3c-8e85-dda1d7758434"

[compat]
julia = "^1.1"
Aqua = "0.8"
ExplicitImports = "1"
LinearAlgebra = "1"
RegisterCore = "0.2"
Test = "1"
julia = "1.10"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]
test = ["Aqua", "ExplicitImports", "Test"]
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# RegisterUtilities.jl

[![CI](https://github.com/HolyLab/RegisterUtilities.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/HolyLab/RegisterUtilities.jl/actions/workflows/CI.yml)
[![codecov](https://codecov.io/gh/HolyLab/RegisterUtilities.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/HolyLab/RegisterUtilities.jl)
[![version](https://juliahub.com/docs/General/RegisterUtilities/stable/version.svg)](https://juliahub.com/ui/Packages/General/RegisterUtilities)

Utility types and functions for image-registration workflows in the
[HolyLab](https://github.com/HolyLab) ecosystem.

## Installation

This package depends on
[RegisterCore.jl](https://github.com/HolyLab/RegisterCore.jl), which lives in
the [HolyLab registry](https://github.com/HolyLab/HolyLabRegistry). Add that
registry once before installing:

```julia
using Pkg
pkg"registry add General https://github.com/HolyLab/HolyLabRegistry.git"
Pkg.add("RegisterUtilities")
```

## Usage

### `Counter` — column-major multi-dimensional index iterator

`Counter` yields every `Vector{Int}` index from `[1,1,…,1]` to `max` in
column-major (first-index-fastest) order. Unlike `CartesianIndices`, the
yielded values are plain vectors, which is convenient for arithmetic.

```julia
using RegisterUtilities

c = Counter((2, 3))
collect(c)
# [[1,1],[2,1],[1,2],[2,2],[1,3],[2,3]]

length(Counter((4, 5, 6))) # 120
```

### `block_center` — center coordinate of a block

Returns the 1-based center of a block with the given dimensions, using the
same convention as `MismatchArray` blocks: center of dimension `i` is
`(sz[i] >> 1) + 1`.

```julia
block_center(5) # (3,)
block_center(4, 6) # (3, 4)
block_center(5, 5, 5) # (3, 3, 3)
```

### `quadratic` — quadratic-form array

Fills a 2-D array where each element `(i,j)` equals `uᵀ Q u`, with
`u = [i,j] - center` and `center = block_center(m,n) .+ shift`. The second
method wraps the result in a `MismatchArray` with a given denominator.

```julia
using RegisterUtilities, LinearAlgebra

Q = [1.0 0.0; 0.0 1.0] # identity — distance² from center
A = quadratic(5, 5, [0, 0], Q)
# 5×5 matrix of squared distances from the block center (3, 3)

using RegisterCore
denom = ones(5, 5)
ma = quadratic(denom, [0, 0], Q) # returns a MismatchArray
```

### `tighten` — narrow element type

Converts a heterogeneous or overly-wide array (e.g. `Array{Real}`) to the
narrowest concrete type that holds all its values.

```julia
A = Real[1, 2.0, 3f0]
B = tighten(A)
eltype(B) # Float64
```
104 changes: 95 additions & 9 deletions src/RegisterUtilities.jl
Original file line number Diff line number Diff line change
@@ -1,22 +1,53 @@
"""
RegisterUtilities

Utility types and functions for image-registration workflows.

Exports:
- [`Counter`](@ref): column-major multi-dimensional index iterator.
- [`quadratic`](@ref): construct a quadratic-form array or `MismatchArray`.
- [`block_center`](@ref): compute the center coordinate of a block.
- [`tighten`](@ref): narrow an array to its tightest concrete element type.
"""
module RegisterUtilities

export Counter

#### Counter ####
#
# Stolen from Grid.jl. Useful when you want to do more math on the iterator.
"""
Counter(max::AbstractVector{<:Integer})
Counter(sz::Tuple)

An iterator that yields every `Vector{Int}` index tuple from `[1, 1, …, 1]`
to `max` (or `collect(sz)`), traversing in column-major (first-index-fastest)
order.

Unlike `CartesianIndices`, `Counter` yields plain `Vector{Int}` values, making
it convenient when the index needs to be used in arithmetic expressions.

`length(c)` returns the total number of elements (`prod(max)`), or `0` if any
dimension is ≤ 0.

# Examples
```julia
c = Counter((2, 3))
collect(c) # [[1,1],[2,1],[1,2],[2,2],[1,3],[2,3]]
```
"""
struct Counter
max::Vector{Int}
end
Counter(sz::Tuple) = Counter(Int[sz...])
Counter(max::AbstractVector{<:Integer}) = Counter(convert(Vector{Int}, max))

Base.length(c::Counter) = isempty(c.max) || any(<=(0), c.max) ? 0 : prod(c.max)

function Base.iterate(c::Counter)
N = length(c.max)
(N == 0 || any(c.max .<= 0)) && return nothing
state = ones(Int, N)
return copy(state), state
end
function Base.iterate(c::Counter, state)
function Base.iterate(c::Counter, state::Vector{Int})
state[1] += 1
i = 1
while state[i] > c.max[i] && i < length(state)
Expand All @@ -30,28 +61,83 @@ end

# Below functions are from RegisterTestUtilities

using RegisterCore, LinearAlgebra
using LinearAlgebra: LinearAlgebra, dot
using RegisterCore: RegisterCore, MismatchArray

export quadratic, block_center, tighten

"""
quadratic(m, n, shift, Q)
quadratic(denom::AbstractMatrix, shift, Q)

Construct a 2-D array of quadratic-form values centered at a shifted block
center.

For each pixel `(i, j)` the value is `uᵀ Q u`, where `u = [i, j] - center`
and `center = block_center(m, n) .+ shift`.

The second method wraps the result in a `MismatchArray` using `denom` as the
denominator array; `m` and `n` are taken from `size(denom)`.

# Arguments
- `m`, `n`: output array dimensions (rows, columns).
- `denom`: an existing `AbstractMatrix` whose size sets the output dimensions
and whose values become the denominator of the returned `MismatchArray`.
- `shift`: 2-element offset added to the block center.
- `Q`: 2×2 matrix defining the quadratic form.
"""
function quadratic(m, n, shift, Q)
A = zeros(m, n)
T = float(eltype(Q))
A = zeros(T, m, n)
c = block_center(m, n)
cntr = [shift[1] + c[1], shift[2] + c[2]]
u = zeros(2)
u = zeros(T, 2)
for j in 1:n, i in 1:m
u[1], u[2] = i - cntr[1], j - cntr[2]
A[i, j] = dot(u, Q * u)
end
return A
end

quadratic(shift, Q, denom::Matrix) = MismatchArray(quadratic(size(denom)..., shift, Q), denom)
quadratic(denom::AbstractMatrix, shift, Q) = MismatchArray(quadratic(size(denom)..., shift, Q), denom)

"""
block_center(sz...)

Return the 1-based center coordinate of a block with dimensions `sz` as an
`NTuple`.

The center of dimension `i` is computed as `(sz[i] >> 1) + 1`, matching the
convention used by `MismatchArray` blocks.

# Examples
```julia
block_center(5) # (3,)
block_center(4, 6) # (3, 4)
block_center(5, 5, 5) # (3, 3, 3)
```
"""
function block_center(sz...)
return ntuple(i -> sz[i] >> 1 + 1, length(sz))
return ntuple(i -> (sz[i] >> 1) + 1, length(sz))
end

"""
tighten(A::AbstractArray)

Return a copy of `A` with the narrowest element type that can hold all of its
values.

Iterates over every element of `A` and accumulates a common type via
`promote_type`, then copies `A` into a new array of that type. Useful for
converting heterogeneous or overly-wide arrays (e.g. `Array{Real}`) into a
concrete, efficient representation.

# Example
```julia
A = Real[1, 2.0, 3f0] # element type Real
B = tighten(A) # element type Float64
```
"""
function tighten(A::AbstractArray)
T = typeof(first(A))
for a in A
Expand Down
58 changes: 58 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
using Test
using Aqua
using ExplicitImports
using LinearAlgebra: I
using RegisterCore: RegisterCore
using RegisterUtilities

@testset "Aqua" begin
Aqua.test_all(RegisterUtilities)
end

@testset "ExplicitImports" begin
test_explicit_imports(RegisterUtilities)
end

@testset "Counter test" begin
for empty_gridsize in ((), (0,), (1, 0), (1, -1), (1, 0, 1))
for c in Counter(empty_gridsize)
Expand All @@ -24,4 +36,50 @@ using RegisterUtilities
end
@test cnt == index_vec
end
# AbstractVector constructor
cnt32 = [c for c in Counter(Int32[2, 3])]
@test cnt32 == [[1,1],[2,1],[1,2],[2,2],[1,3],[2,3]]
end

@testset "block_center" begin
@test block_center(1) == (1,)
@test block_center(2) == (2,)
@test block_center(4) == (3,)
@test block_center(5) == (3,)
@test block_center(4, 6) == (3, 4)
@test block_center(8) == (5,)
end

@testset "quadratic" begin
Q = Matrix(1.0I, 2, 2)
m, n = 5, 7
A = quadratic(m, n, (0, 0), Q)
@test size(A) == (m, n)
c = block_center(m, n)
@test A[c...] == 0.0
@test all(>=(0), A)
@inferred quadratic(m, n, (0, 0), Q)

# shifting the center moves the zero
A2 = quadratic(m, n, (1, 0), Q)
@test A2[c[1] + 1, c[2]] == 0.0

# matrix-denom variant returns a MismatchArray of the right size
denom = ones(m, n)
result = quadratic(denom, (0, 0), Q)
@test result isa RegisterCore.MismatchArray
@test size(result) == (m, n)
end

@testset "tighten" begin
# heterogeneous Any-array gets promoted
A = Any[1, 2.0, 3f0]
result = tighten(A)
@test eltype(result) == Float64
@test result ≈ [1.0, 2.0, 3.0]

# already-concrete array is unchanged in value and type
B = [1, 2, 3]
@test tighten(B) == B
@test eltype(tighten(B)) == Int
end
Loading