• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

darikoneil / deinterlacing / #2

07 Apr 2025 09:35PM UTC coverage: 89.823%. Remained the same
#2

push

coveralls-python

darikoneil
initial test suite.

41 of 52 branches covered (78.85%)

Branch coverage included in aggregate %.

21 of 23 new or added lines in 2 files covered. (91.3%)

9 existing lines in 2 files now uncovered.

162 of 174 relevant lines covered (93.1%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.08
/deinterlacing/processing.py
1
from dataclasses import InitVar
1✔
2
from functools import partial
1✔
3
from math import inf
1✔
4
from typing import Literal
1✔
5

6
import numpy as np
1✔
7
from pydantic import ConfigDict, Field, field_validator
1✔
8
from pydantic.dataclasses import dataclass
1✔
9
from tqdm import tqdm
1✔
10

11
from deinterlacing.tools import (
1✔
12
    NDArrayLike,
13
    ParameterError,
14
    align_pixels,
15
    align_subpixels,
16
    calculate_offset_matrix,
17
    extract_image_block,
18
    find_pixel_offset,
19
    find_subpixel_offset,
20
    index_image_blocks,
21
    wrap_cupy,
22
)
23

24
try:
1✔
25
    import cupy as cp
1✔
26
except ImportError:
27
    cp = np
28

29

30
__all__ = [
1✔
31
    "DeinterlaceParameters",
32
    "deinterlace",
33
]
34

35

36
@dataclass(slots=True, config=ConfigDict(arbitrary_types_allowed=True))
1✔
37
class DeinterlaceParameters:
1✔
38
    #: Number of frames included per FFT calculation.
39
    block_size: int | None = None
1✔
40
    #: Whether to apply subsampling
41
    pool: Literal["mean", "median", "std", "sum", None] = None
1✔
42
    #: Number of frames to deinterlace individually before switching to batch-wise
43
    #: processing.
44
    unstable: int | None = None
1✔
45
    #: Subsearc
46
    subsearch: int | None = 15
1✔
47
    #: Align
48
    align: Literal["pixel", "subpixel"] = "pixel"
1✔
49
    #: use gpu
50
    use_gpu: bool = False
1✔
51
    #: images to validate against
52
    images: InitVar[NDArrayLike | None] = None
1✔
53

54
    def __post_init__(self, images: NDArrayLike | None) -> None:
1✔
55
        if images is not None:
1✔
56
            self.validate_with_images(images)
1✔
57

58
    @field_validator("block_size", "unstable", "subsearch", mode="after")
1✔
59
    @classmethod
1✔
60
    def _validate_positive_integer(cls, value: int | None, ctx: Field) -> int | None:
1✔
61
        """
62
        Validate that the given value is a positive integer or None.
63

64
        :param value: The value to validate, which can be an integer or None.
65
        :returns: The validated value, or None if the input was None.
66
        """
67
        if value is not None and value <= 0:
1✔
68
            raise ParameterError(parameter=ctx.field_name, value=value, limits=(0, inf))
1✔
69
        return value
1✔
70

71
    def validate_with_images(self, images: NDArrayLike) -> None:
1✔
72
        """
73
        Validate the parameters against the provided images..
74

75
        :param images: The images to validate against.
76
        :returns: None
77
        """
78
        # BLOCK SIZE
79
        if self.block_size is None:
1✔
80
            self.block_size = images.shape[0]
1✔
81
        if self.block_size > images.shape[0]:
1✔
82
            raise ParameterError(
1✔
83
                parameter="block_size",
84
                value=self.block_size,
85
                limits=(1, images.shape[0]),
86
            )
87

88
        # SUBSEARCH
89
        if self.subsearch is None:
1✔
90
            min_dim = min(images.shape[1:])  # Get the minimum spatial dimension
1✔
91
            self.subsearch = min_dim // 16
1✔
92
        if self.subsearch > min(images.shape[1:]):
1✔
93
            raise ParameterError(
1✔
94
                parameter="subsearch",
95
                value=self.subsearch,
96
                limits=(1, min(images.shape[1:]) - 1),
97
            )
98

99
        # UNSTABLE
100
        if self.unstable is not None and self.unstable > images.shape[0]:
1✔
101
            raise ParameterError(
1✔
102
                parameter="unstable",
103
                value=self.unstable,
104
                limits=(0, images.shape[0]),
105
            )
106

107
        # USE GPU
108
        if self.use_gpu and cp == np:
1✔
109
            msg = "CuPy is not available. GPU acceleration cannot be used."
1✔
110
            raise ValueError(msg)
1✔
111

112

113
def deinterlace(
1✔
114
    images: NDArrayLike,
115
    parameters: DeinterlaceParameters | None = None,
116
) -> None:
117
    """
118
    Deinterlace images collected using resonance-scanning microscopes such that the
119
    forward and backward-scanned lines are properly aligned. A fourier-approach is
120
    utilized: the fourier transform of the two sets of lines is computed to calculate
121
    the cross-power spectral density. Taking the inverse fourier transform of the
122
    cross-power spectral density yields a matrix whose peak corresponds to the
123
    sub-pixel offset between the two sets of lines. This translative offset was then
124
    discretized and used to shift the backward-scanned lines.
125

126
    Unfortunately, the fast-fourier transform methods that underlie the implementation
127
    of the deinterlacing algorithm have poor spatial complexity
128
    (i.e., large memory constraints). This weakness is particularly problematic when
129
    using GPU-parallelization. To mitigate these issues, deinterlacing can be performed
130
    batch-wise while maintaining numerically identical results (see `block_size`).
131

132
    To improve performance, the deinterlacing algorithm can be applied to a pool
133
    of the images while maintaining efficacy. Specifically, setting the `pool`
134
    parameter will apply the deinterlacing algorithm to the the standard deviation of
135
    each pixel across a block of images. This approach is better suited to images with
136
    limited signal-to-noise or sparse activity than simply operating on every n-th
137
    frame.
138

139
    Finally, it is often the case that the auto-alignment algorithms used in microscopy
140
    software are unstable until a sufficient number of frames have been collected.
141
    Therefore, the `unstable` parameter can be used to specify the number of frames
142
    that should be deinterlaced individually before switching to batch-wise processing.
143

144
    .. note::
145
        This function operates in-place.
146

147
    .. warning::
148
        The number of frames included in each fourier transform must be several times
149
        smaller than the maximum number of frames that fit within your GPU's VRAM
150
        (`CuPy <https://cupy.dev>`_) or RAM (`NumPy <https://numpy.org>`_). This
151
        function will not automatically revert to the NumPy implementation if there is
152
        not sufficient VRAM. Instead, an out of memory error will be raised.
153
    """
154
    parameters = parameters or DeinterlaceParameters()
1✔
155
    parameters.validate_with_images(images)
1✔
156

157
    # Set implementations for calculations
158
    match (parameters.align, parameters.use_gpu):
1✔
159
        case ("pixel", False):
1✔
160
            calculate_matrix = partial(calculate_offset_matrix, fft_module=np)
1✔
161
            find_peak = find_pixel_offset
1✔
162
            align_images = align_pixels
1✔
163
        case ("pixel", True):
1✔
164
            calculate_matrix = wrap_cupy(
1✔
165
                partial(calculate_offset_matrix, fft_module=cp), "images"
166
            )
167
            find_peak = find_pixel_offset
1✔
168
            align_images = align_pixels
1✔
169
        case ("subpixel", False):
1✔
170
            calculate_matrix = partial(calculate_offset_matrix, fft_module=np)
1✔
171
            find_peak = find_subpixel_offset
1✔
172
            align_images = partial(align_subpixels, fft_module=np)
1✔
173
        case ("subpixel", True):
1!
174
            calculate_matrix = wrap_cupy(
1✔
175
                partial(calculate_offset_matrix, fft_module=cp), "images"
176
            )
177
            find_peak = find_subpixel_offset
1✔
178
            align_images = partial(align_subpixels, fft_module=cp)
1✔
UNCOV
179
        case _:
×
180
            msg = (
×
181
                f"Invalid combination of align='{parameters.align}' and use_gpu={parameters.use_gpu}. "
182
                "Align must be either 'pixel' or 'subpixel', and use_gpu must be a boolean."
183
            )
UNCOV
184
            raise ValueError(msg)
×
185

186
    # now iterate
187
    pbar = tqdm(total=images.shape[0], desc="Deinterlacing Images", colour="blue")
1✔
188
    for start, stop in index_image_blocks(
1✔
189
        images, parameters.block_size, parameters.unstable
190
    ):
191
        # NOTE: Extraction isn't done inline due to the 'pool' parameter potentially
192
        #  changing the shape of the images being processed. In some cases this means
193
        #  the returned block_images will not be views of the original images, but
194
        #  currently this only occurs when reducing the number of frames to process
195
        #  through pool.If adding a feature here in the future (e.g., upscaling), one
196
        #  will need to remember this is no view guarantee here.
197
        block_images = extract_image_block(images, start, stop, parameters.pool)
1✔
198
        offset_matrix = calculate_matrix(block_images)
1✔
199
        offset = find_peak(block_images, offset_matrix, parameters.subsearch)
1✔
200
        align_images(images, start, stop, offset)
1✔
201
        pbar.update(stop - start)
1✔
202
    pbar.close()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc