nanoobjectdetection 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ """Chunked TIFF storage for the independent pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Iterator
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+
12
+ from .progress import progress
13
+
14
+
15
+ def _tifffile():
16
+ try:
17
+ import tifffile
18
+ except ImportError as exc:
19
+ raise RuntimeError("Install 'tifffile' to use TIFF storage.") from exc
20
+ return tifffile
21
+
22
+
23
+ def tiff_info(path: Path) -> dict[str, Any]:
24
+ tifffile = _tifffile()
25
+ with tifffile.TiffFile(path) as tif:
26
+ series = tif.series[0]
27
+ return {
28
+ "path": str(path.resolve()),
29
+ "shape": tuple(series.shape),
30
+ "axes": series.axes,
31
+ "dtype": str(series.dtype),
32
+ "pages": len(tif.pages),
33
+ }
34
+
35
+
36
+ def to_processing_bit_depth(
37
+ frames: np.ndarray, processing_bit_depth: int
38
+ ) -> np.ndarray:
39
+ """Convert unsigned TIFF frames to the selected processing bit depth."""
40
+ if processing_bit_depth == 16:
41
+ return frames
42
+ if processing_bit_depth != 8:
43
+ raise ValueError("processing_bit_depth must be 8 or 16")
44
+
45
+ data = np.asarray(frames)
46
+ if data.dtype.kind != "u":
47
+ raise ValueError(
48
+ "8-bit processing requires an unsigned 8-bit or 16-bit TIFF stack"
49
+ )
50
+ if data.dtype.itemsize == 1:
51
+ return data
52
+ if data.dtype.itemsize == 2:
53
+ return np.right_shift(data, 8).astype(np.uint8)
54
+ raise ValueError(
55
+ "8-bit processing requires an unsigned 8-bit or 16-bit TIFF stack"
56
+ )
57
+
58
+
59
+ def read_frames(path: Path, first: int, stop: int) -> np.ndarray:
60
+ tifffile = _tifffile()
61
+ try:
62
+ mapped = tifffile.memmap(path, mode="r")
63
+ result = np.array(mapped[first:stop], copy=True)
64
+ del mapped
65
+ return result
66
+ except (OSError, ValueError, RuntimeError):
67
+ return np.asarray(tifffile.imread(path, key=slice(first, stop)))
68
+
69
+
70
+ def iter_tiff_frames(
71
+ path: Path, frame_start: int, frame_stop: int | None
72
+ ) -> Iterator[tuple[int, np.ndarray]]:
73
+ """Yield source TIFF frames sequentially without creating a chunk store."""
74
+ tifffile = _tifffile()
75
+ with tifffile.TiffFile(path) as tif:
76
+ series = tif.series[0]
77
+ total = int(series.shape[0]) if len(series.shape) > 2 else len(tif.pages)
78
+ stop = total if frame_stop is None else min(frame_stop, total)
79
+ if frame_start < 0 or frame_start >= stop:
80
+ raise ValueError("Configured frame range is outside the source TIFF")
81
+ if len(tif.pages) >= total:
82
+ for frame_number in range(frame_start, stop):
83
+ yield frame_number, tif.pages[frame_number].asarray()
84
+ return
85
+ # ImageJ stacks store all frames behind a single page header; map them directly
86
+ mapped = tifffile.memmap(path, mode="r")
87
+ try:
88
+ for frame_number in range(frame_start, stop):
89
+ yield frame_number, np.array(mapped[frame_number])
90
+ finally:
91
+ del mapped
92
+
93
+
94
+ def create_store(
95
+ source: Path,
96
+ target: Path,
97
+ frame_start: int,
98
+ frame_stop: int | None,
99
+ chunk_frames: int,
100
+ chunk_overlap_frames: int,
101
+ y_slice: slice,
102
+ x_slice: slice,
103
+ processing_bit_depth: int = 16,
104
+ ) -> dict[str, Any]:
105
+ """Create a reproducible chunk store without changing the source."""
106
+ info = tiff_info(source)
107
+ total = int(info["shape"][0])
108
+ stop = total if frame_stop is None else min(frame_stop, total)
109
+ if frame_start >= stop:
110
+ raise ValueError("Configured frame range is outside the source TIFF")
111
+
112
+ chunks_dir = target / "chunks"
113
+ chunks_dir.mkdir(parents=True, exist_ok=True)
114
+ tifffile = _tifffile()
115
+ manifest: dict[str, Any] = {
116
+ "format": "nanoobjectdetection-2.0-chunks-v1",
117
+ "source": str(source.resolve()),
118
+ "axes": info["axes"],
119
+ "source_dtype": info["dtype"],
120
+ "dtype": (
121
+ "uint8"
122
+ if processing_bit_depth == 8 and np.dtype(info["dtype"]).itemsize == 2
123
+ else info["dtype"]
124
+ ),
125
+ "frame_start": frame_start,
126
+ "frame_stop": stop,
127
+ "chunk_frames": chunk_frames,
128
+ "chunk_overlap_frames": chunk_overlap_frames,
129
+ "shape": None,
130
+ "chunks": [],
131
+ }
132
+ step = chunk_frames - chunk_overlap_frames
133
+ chunk_starts = range(frame_start, stop, step)
134
+ total_chunks = (stop - frame_start + step - 1) // step
135
+ for chunk_number, first in enumerate(
136
+ progress(
137
+ chunk_starts,
138
+ desc="Writing TIFF chunks",
139
+ total=total_chunks,
140
+ unit="chunk",
141
+ )
142
+ ):
143
+ last = min(first + chunk_frames, stop)
144
+ data = read_frames(source, first, last)
145
+ data = data[:, y_slice, x_slice]
146
+ data = to_processing_bit_depth(data, processing_bit_depth)
147
+ if manifest["shape"] is None:
148
+ manifest["shape"] = [stop - frame_start, data.shape[1], data.shape[2]]
149
+ name = f"chunk_{chunk_number:06d}.tif"
150
+ tifffile.imwrite(
151
+ chunks_dir / name,
152
+ data,
153
+ compression="deflate",
154
+ predictor=True,
155
+ metadata={"axes": "ZYX"},
156
+ )
157
+ manifest["chunks"].append(
158
+ {
159
+ "file": f"chunks/{name}",
160
+ "frame_start": first,
161
+ "frame_stop": last,
162
+ "shape": list(data.shape),
163
+ }
164
+ )
165
+ del data
166
+
167
+ (target / "manifest.json").write_text(
168
+ json.dumps(manifest, indent=2), encoding="utf-8"
169
+ )
170
+ return manifest
171
+
172
+
173
+ def iter_blocks(store: Path) -> Iterator[tuple[int, np.ndarray]]:
174
+ manifest_path = store / "manifest.json"
175
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
176
+ if manifest.get("format") != "nanoobjectdetection-2.0-chunks-v1":
177
+ raise ValueError(f"Unsupported image store: {store}")
178
+ for chunk in progress(
179
+ manifest["chunks"],
180
+ desc="Reading image chunks",
181
+ total=len(manifest["chunks"]),
182
+ unit="chunk",
183
+ ):
184
+ yield (
185
+ int(chunk["frame_start"]),
186
+ read_frames(store / chunk["file"], 0, int(chunk["shape"][0])),
187
+ )
@@ -0,0 +1,28 @@
1
+ """Deterministic synthetic trajectories for numerical pipeline tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+
9
+ def brownian_trajectories(
10
+ particles: int = 3,
11
+ frames: int = 200,
12
+ diffusion_um2_s: float = 1.0,
13
+ fps: float = 300.0,
14
+ seed: int = 42,
15
+ drift_per_frame: tuple[float, float] = (0.0, 0.0),
16
+ ) -> pd.DataFrame:
17
+ """Generate linked trajectories in pixel coordinates."""
18
+ rng = np.random.default_rng(seed)
19
+ rows = []
20
+ step = np.sqrt(2 * diffusion_um2_s / fps)
21
+ for particle in range(particles):
22
+ positions = np.zeros((frames, 2))
23
+ positions[0] = rng.normal(0, 1, 2)
24
+ positions[1:] = positions[0] + np.cumsum(rng.normal(0, step, (frames - 1, 2)), axis=0)
25
+ positions += np.arange(frames)[:, None] * drift_per_frame
26
+ for frame, (x, y) in enumerate(positions):
27
+ rows.append({"particle": particle, "frame": frame, "x": x, "y": y})
28
+ return pd.DataFrame(rows)
@@ -0,0 +1,75 @@
1
+ """Trajectory linking for the independent pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pandas as pd
6
+ import sys
7
+ import numpy as np
8
+
9
+
10
+ def link(
11
+ detections: pd.DataFrame,
12
+ search_range: float,
13
+ memory: int = 1,
14
+ minimum_frames: int = 100,
15
+ adaptive_stop: float | None = None,
16
+ adaptive_step: float = 0.9,
17
+ ) -> pd.DataFrame:
18
+ """Link detections without importing any historical project module."""
19
+ try:
20
+ import trackpy as tp
21
+ from trackpy.linking.utils import SubnetOversizeException
22
+ except ImportError as exc:
23
+ raise RuntimeError(
24
+ "Trajectory linking requires 'trackpy', but it is not "
25
+ f"installed in the active interpreter ({sys.executable}). "
26
+ "Install it with: python -m pip install trackpy"
27
+ ) from exc
28
+ tp.quiet()
29
+ required = {"frame", "x", "y"}
30
+ missing = required.difference(detections.columns)
31
+ if missing:
32
+ raise ValueError(f"Detections are missing columns: {sorted(missing)}")
33
+ if search_range <= 0:
34
+ raise ValueError("search_range must be positive")
35
+ if not 0 < adaptive_step < 1:
36
+ raise ValueError("adaptive_step must be between 0 and 1")
37
+ if adaptive_stop is None:
38
+ adaptive_stop = min(0.5, search_range * 0.25)
39
+ if not 0 < adaptive_stop < search_range:
40
+ raise ValueError("adaptive_stop must be positive and smaller than search_range")
41
+ try:
42
+ linked = tp.link_df(
43
+ detections.sort_values("frame"),
44
+ search_range=search_range,
45
+ memory=memory,
46
+ adaptive_stop=adaptive_stop,
47
+ adaptive_step=adaptive_step,
48
+ )
49
+ except SubnetOversizeException as exc:
50
+ raise RuntimeError(
51
+ "Trackpy could not resolve a dense linking subnetwork even after "
52
+ f"adaptive search-range reduction to {adaptive_stop:g} pixels. "
53
+ "Reduce search_range, increase particle_minmass, or inspect "
54
+ "overlapping detections."
55
+ ) from exc
56
+ lengths = linked.groupby("particle")["frame"].transform("size")
57
+ return linked[lengths >= minimum_frames].reset_index(drop=True)
58
+
59
+
60
+ def remove_near_stationary(
61
+ detections: pd.DataFrame,
62
+ stationary: pd.DataFrame,
63
+ distance: float,
64
+ ) -> pd.DataFrame:
65
+ """Remove detections inside the 1.0 stationary-particle no-go areas."""
66
+ if distance < 0:
67
+ raise ValueError("distance must be non-negative")
68
+ if stationary.empty or detections.empty:
69
+ return detections.copy()
70
+ centers = stationary.groupby("particle")[["x", "y"]].mean().to_numpy()
71
+ points = detections[["x", "y"]].to_numpy()
72
+ keep = np.ones(len(points), dtype=bool)
73
+ for center in centers:
74
+ keep &= np.hypot(points[:, 0] - center[0], points[:, 1] - center[1]) > distance
75
+ return detections.loc[keep].reset_index(drop=True)
@@ -0,0 +1,81 @@
1
+ """Trajectory filtering and splitting compatible with the 1.0 workflow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+
9
+ def filter_minimum_frames(
10
+ trajectories: pd.DataFrame, minimum_frames: int
11
+ ) -> pd.DataFrame:
12
+ """Keep trajectories with at least ``minimum_frames`` measured positions."""
13
+ if minimum_frames < 1:
14
+ raise ValueError("minimum_frames must be positive")
15
+ if trajectories.empty:
16
+ return trajectories.copy()
17
+ lengths = trajectories.groupby("particle")["frame"].transform("size")
18
+ return trajectories.loc[lengths >= minimum_frames].reset_index(drop=True)
19
+
20
+
21
+ def _interpolated_mass(group: pd.DataFrame) -> pd.Series:
22
+ indexed = group.sort_values("frame").set_index("frame")["mass"].astype(float)
23
+ full = indexed.reindex(range(int(indexed.index.min()), int(indexed.index.max()) + 1))
24
+ return full.interpolate(method="nearest").bfill().ffill()
25
+
26
+
27
+ def split_intensity_jumps(
28
+ trajectories: pd.DataFrame,
29
+ relative_step: float,
30
+ minimum_frames: int,
31
+ ) -> pd.DataFrame:
32
+ """Split tracks at large relative mass jumps and discard short fragments."""
33
+ if relative_step <= 0:
34
+ raise ValueError("relative_step must be positive")
35
+ required = {"particle", "frame", "mass"}
36
+ missing = required.difference(trajectories.columns)
37
+ if missing:
38
+ raise ValueError(f"Trajectories are missing columns: {sorted(missing)}")
39
+
40
+ result = trajectories.copy().sort_values(["particle", "frame"])
41
+ next_particle = int(result["particle"].max()) + 1 if not result.empty else 0
42
+ for particle, group in list(result.groupby("particle", sort=False)):
43
+ mass = _interpolated_mass(group)
44
+ smooth = mass.rolling(3, center=True, min_periods=1).median()
45
+ previous = smooth.shift(1)
46
+ relative = (smooth - previous).abs() / ((smooth + previous.abs()) / 2.0)
47
+ split_frames = relative.index[relative > relative_step].tolist()
48
+ for frame in split_frames:
49
+ mask = (result["particle"] == particle) & (result["frame"] >= frame)
50
+ result.loc[mask, "particle"] = next_particle
51
+ next_particle += 1
52
+ particle = next_particle - 1
53
+ return filter_minimum_frames(result, minimum_frames)
54
+
55
+
56
+ def split_long_trajectories(
57
+ trajectories: pd.DataFrame,
58
+ maximum_frames: int | None,
59
+ minimum_frames: int,
60
+ keep_tail: bool = False,
61
+ ) -> pd.DataFrame:
62
+ """Split long tracks into bounded contiguous frame segments."""
63
+ if maximum_frames is None:
64
+ return trajectories.copy()
65
+ if maximum_frames < minimum_frames:
66
+ raise ValueError("maximum_frames must be at least minimum_frames")
67
+ result_parts: list[pd.DataFrame] = []
68
+ next_particle = int(trajectories["particle"].max()) + 1 if not trajectories.empty else 0
69
+ for _, group in trajectories.sort_values(["particle", "frame"]).groupby("particle"):
70
+ group = group.copy()
71
+ for start in range(0, len(group), maximum_frames):
72
+ part = group.iloc[start : start + maximum_frames].copy()
73
+ if len(part) < minimum_frames and not keep_tail:
74
+ continue
75
+ if start:
76
+ part["particle"] = next_particle
77
+ next_particle += 1
78
+ result_parts.append(part)
79
+ if not result_parts:
80
+ return trajectories.iloc[0:0].copy()
81
+ return pd.concat(result_parts, ignore_index=True)
@@ -0,0 +1,52 @@
1
+ """Ultra-uniform particle hypothesis filter (z-score based, as in 1.0 PostProcessing)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ # 99 % two-sided confidence level
9
+ DEFAULT_Z_MAX = 2.576
10
+
11
+
12
+ def _spread(values: np.ndarray) -> float:
13
+ return float(np.sqrt(np.mean((values - np.median(values)) ** 2)))
14
+
15
+
16
+ def _remove_outliers(data: pd.DataFrame, column: str, roll_area: int, z_max: float) -> pd.DataFrame:
17
+ """Iteratively drop rows whose z-score (median-centred, rolling spread) reaches z_max."""
18
+ while len(data):
19
+ spread = data[column].rolling(roll_area, min_periods=1, center=True).apply(_spread, raw=True)
20
+ z = (data[column] - data[column].median()).abs() / spread
21
+ if z.max() < z_max:
22
+ break
23
+ data = data[z < z_max]
24
+ return data
25
+
26
+
27
+ def ultra_uniform_mask(
28
+ estimates: pd.DataFrame,
29
+ z_max: float = DEFAULT_Z_MAX,
30
+ diameter_column: str = "diameter_nm",
31
+ diffusion_column: str = "diffusion_um2_s",
32
+ order_column: str = "valid_frames",
33
+ ) -> pd.Series:
34
+ """Return a boolean mask of particles that fit a single uniform particle species.
35
+
36
+ Particles are ordered by trajectory precision, then diameter and diffusion are
37
+ filtered one after the other (diffusion on the particles that survived the
38
+ diameter step), exactly like ForceUltraUniformParticle in 1.0.
39
+ """
40
+ valid = estimates[estimates[diameter_column].notna() & estimates[diffusion_column].notna()]
41
+ num_particles = len(valid)
42
+ if num_particles < 10:
43
+ return pd.Series(True, index=estimates.index)
44
+ roll_area = 100 if num_particles >= 100 else num_particles - 2
45
+ data = valid[[diameter_column, diffusion_column, order_column]].sort_values(order_column)
46
+ data = _remove_outliers(data, diameter_column, roll_area, z_max)
47
+ data = _remove_outliers(data, diffusion_column, roll_area, z_max)
48
+ keep = pd.Series(False, index=estimates.index)
49
+ keep.loc[data.index] = True
50
+ # Rows without a valid estimate were already rejected earlier and are left untouched
51
+ keep.loc[~estimates.index.isin(valid.index)] = True
52
+ return keep
@@ -0,0 +1,249 @@
1
+ Metadata-Version: 2.4
2
+ Name: nanoobjectdetection
3
+ Version: 2.0.0
4
+ Summary: Particle localization and diffusion analysis for TIFF image stacks
5
+ Author: Tim Stechel, Ronny Förster
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/knobold/NanoObjectDetection
8
+ Project-URL: Repository, https://github.com/knobold/NanoObjectDetection
9
+ Project-URL: Issues, https://github.com/knobold/NanoObjectDetection/issues
10
+ Keywords: nanoparticles,microscopy,diffusion,tracking,stokes-einstein,trackpy,tiff
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
14
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
15
+ Classifier: Topic :: Scientific/Engineering :: Physics
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: numpy
23
+ Requires-Dist: pandas
24
+ Requires-Dist: scipy
25
+ Requires-Dist: tifffile
26
+ Requires-Dist: trackpy
27
+ Requires-Dist: tqdm
28
+ Requires-Dist: matplotlib
29
+ Requires-Dist: numba
30
+ Provides-Extra: gpu
31
+ Requires-Dist: cupy-cuda12x; extra == "gpu"
32
+
33
+ # NanoObjectDetection 2.0
34
+
35
+ This is a new Version of Ronny Foersters Package. The old Package had lots of issues with type safety and RAM usage. This new implementation aims to improve RAM usage and enable us to easily develop new features.
36
+
37
+ NanoObjectDetection analyzes TIFF image stacks of diffusing particles. It
38
+ localizes particles, links detections into trajectories, estimates diffusion
39
+ and hydrodynamic diameter, and exports analysis tables and plots. Version 2.0
40
+ keeps compatibility with the 1.0 parameter format and adds blockwise
41
+ processing and optional CUDA acceleration.
42
+
43
+ The source checkout also contains the original 1.0 implementation in
44
+ [`nanoobjectdetection_legacy/`](./nanoobjectdetection_legacy/); the installable
45
+ distribution packages the modern pipeline under the `nanoobjectdetection`
46
+ package. For full parameter descriptions,
47
+ scientific methods, compatibility details, limitations, and output-file
48
+ definitions, see the
49
+ [2.0 documentation](./NANOOBJECTDETECTION_2_0_DOCUMENTATION.md).
50
+
51
+ ## Features
52
+
53
+ - TIFF stack input with spatial and frame-range ROI selection.
54
+ - Camera-offset, laser-fluctuation, static-background, and optional SNR
55
+ preprocessing.
56
+ - Trackpy particle localization and trajectory linking, including optional
57
+ stitching for repeatedly lost single-particle detections.
58
+ - Fixed-particle filtering, trajectory quality checks, and global drift
59
+ correction.
60
+ - Mean-squared displacement analysis, diffusion estimation, and
61
+ Stokes-Einstein hydrodynamic diameter calculations.
62
+ - Optional channel hindrance correction, including position-aware correction.
63
+ - Single Particle Tracking using stitched Trajectories
64
+ - RAM saving options: Single-stack and chunked execution modes, plus optional CUDA preprocessing, Optional Conversion of pixels to 8bit depth, 16bit is probably overkill.
65
+ - CSV tables, compatibility output names, run configuration, and plots.
66
+
67
+ ## New Features
68
+
69
+ - Try to implement them on your own! AI is powerful and easy too use. You can then try to submit a merge request here.
70
+
71
+ ## Issues
72
+
73
+ - 1st Try to solve the issue yourself with AI
74
+ -> if its a bug caused by this repo, not your local installation open an Issue or write me a detailed Bug report on Slack. Paste console logs or anything that helps me solve your issue.
75
+
76
+ ## Installation
77
+
78
+ ### From PyPI (Recommended)
79
+
80
+ Install the latest release directly via pip:
81
+
82
+ ```bash
83
+ python -m pip install --upgrade pip
84
+ python -m pip install nanoobjectdetection
85
+ ```
86
+
87
+ For GPU acceleration with CUDA 12.x:
88
+
89
+ ```bash
90
+ python -m pip install "nanoobjectdetection[gpu]"
91
+ ```
92
+
93
+ ### From Source Checkout
94
+
95
+ Download this repo as a `.zip` file and unpack it, or clone via Git.
96
+ From the repository folder, activate your environment and install:
97
+
98
+ ```bash
99
+ python -m pip install --upgrade pip
100
+ python -m pip install .
101
+ ```
102
+
103
+ To install an editable development copy instead, use `python -m pip install -e .` from the repository folder.
104
+
105
+ The installation adds the `nanoobjectdetection` CLI command. It can be run from
106
+ any working directory:
107
+
108
+ ```bash
109
+ nanoobjectdetection --input-file "path/to/stack.tif" --output-dir "path/to/output"
110
+ ```
111
+
112
+ ### Python Example & Scripting
113
+
114
+ The recommended way to use the package is to adjust analysis parameters in an example script. Run the included [`example/example.py`](./example/example.py) from any directory:
115
+
116
+ ```powershell
117
+ python "example/example.py" `
118
+ --input-file "path/to/my_stack.tif" `
119
+ --output-dir "path/to/my_analysis" `
120
+ --background-stack-storage disk `
121
+ --processing-bit-depth 8 `
122
+ --dimension all
123
+ ```
124
+
125
+ The script imports `run` directly from the installed package:
126
+ ```python
127
+ from nanoobjectdetection import run
128
+ ```
129
+ Its `PARAMETERS` dictionary and processing settings near the top of
130
+ `example/example.py` expose the full analysis configuration for editing. Omit
131
+ `--parameter-file` to use those settings; optionally add
132
+ `--parameter-file "path/to/parameter.json"` to use a legacy JSON instead.
133
+ Input/output paths can be configured directly inside the script or passed as CLI options.
134
+ Run `python example/example.py --help` for all options.
135
+
136
+ ### Optional GPU preprocessing
137
+
138
+ GPU mode requires a supported NVIDIA GPU, a compatible CUDA runtime, and the
139
+ matching CuPy package. For CUDA 12.x, install the package's GPU extra:
140
+
141
+ ```bash
142
+ python -m pip install ".[gpu]"
143
+ ```
144
+
145
+ If your CUDA version differs, install the CuPy package matching your CUDA
146
+ runtime instead. The pipeline reports an error if GPU mode is requested but a
147
+ usable CUDA device is unavailable. Trackpy localization, linking, and MSD
148
+ fitting remain CPU operations.
149
+
150
+ ## Run from an IDE
151
+
152
+ The source `main.py` defaults to the included example TIFF and can be run
153
+ directly from VS Code, PyCharm, or another Python IDE:
154
+
155
+ 1. Open the repository folder in your IDE.
156
+ 2. Select the Python interpreter from the `.venv` environment created above.
157
+ In VS Code, use **Python: Select Interpreter**; in PyCharm, select the
158
+ project interpreter in the Python interpreter settings.
159
+ 3. Run `main.py` with the IDE's **Run Python File** or **Run** action to
160
+ analyze the included example.
161
+
162
+ For your own data, use the installed command with a parameter JSON rather than
163
+ editing `main.py`. Review camera calibration, acquisition rate, temperature,
164
+ viscosity, channel dimensions, ROI, and Trackpy settings in that JSON.
165
+
166
+ ## Run from a terminal
167
+
168
+ Run an installed analysis from any folder in PowerShell:
169
+
170
+ ```powershell
171
+ nanoobjectdetection `
172
+ --parameter-file "C:\data\parameter.json" `
173
+ --input-file "C:\data\my_stack.tif" `
174
+ --output-dir "C:\data\my_analysis" `
175
+ --processing-mode single_stack `
176
+ --background-stack-storage disk `
177
+ --processing-bit-depth 8
178
+ ```
179
+
180
+ To install the package from another folder or drive, use its repository path:
181
+
182
+ ```powershell
183
+ python -m pip install "D:\Python\NanoObjectDetection"
184
+ ```
185
+
186
+ The source script can also be run with options from the repository folder:
187
+
188
+ ```powershell
189
+ python .\main.py `
190
+ --input-file "C:\data\my_stack.tif" `
191
+ --output-dir "C:\data\my_analysis" `
192
+ --processing-mode single_stack `
193
+ --dimension 2d `
194
+ --background-stack-storage disk
195
+ ```
196
+
197
+ To use a legacy-format parameter file:
198
+
199
+ ```powershell
200
+ python .\main.py `
201
+ --parameter-file ".\example\parameter.json" `
202
+ --input-file ".\example\20_6µmKap_50Au_20_7C_100fps_5000fr_700us_V1j.tif" `
203
+ --output-dir ".\example\my_analysis" `
204
+ --processing-mode chunked
205
+ ```
206
+
207
+ The input and output command-line paths override the corresponding paths in
208
+ the parameter JSON. If `--parameter-file` is omitted, the analysis settings
209
+ defined in `main.py` are used.
210
+
211
+ Available command-line options:
212
+
213
+ | Option | Purpose |
214
+ |---|---|
215
+ | `--processing-mode single_stack` | Read frames directly from the TIFF; this is the default. |
216
+ | `--processing-mode chunked` | Store/process the selected stack in blocks, useful for large inputs. |
217
+ | `--background-stack-storage memory` | Keep the background stack used for subtraction in RAM (default). |
218
+ | `--background-stack-storage disk` | Use a temporary disk-backed stack; needs free space approximately equal to the selected-depth ROI stack. |
219
+ | `--processing-bit-depth 16` | Preserve the input pixel values (default). |
220
+ | `--processing-bit-depth 8` | For uint16 input, keep the high 8 bits (`value >> 8`) for stored/raw processing data; halves raw/background stack storage but discards low-bit precision. |
221
+ | `--dimension all` | Analyze x, y, and 2d trajectories; this is the default. |
222
+ | `--dimension x`, `--dimension y`, `--dimension 2d` | Calculate only the selected trajectory dimension. |
223
+ | `--gpu` | Enable GPU preprocessing; requires compatible CuPy/CUDA installation. |
224
+ | `--input-file PATH` | Select the TIFF stack to analyze. |
225
+ | `--output-dir PATH` | Select where results are written. |
226
+ | `--parameter-file PATH` | Load analysis settings from a legacy-format JSON file. |
227
+
228
+ Disk-backed background storage preserves the same calculation as memory
229
+ storage at the selected bit depth and removes its temporary stack file after
230
+ localization. It can use more disk I/O and run more slowly than keeping the
231
+ stack in RAM.
232
+
233
+ 8-bit processing is opt-in and quantizes uint16 values by shifting right eight
234
+ bits; it is not equivalent to the default 16-bit analysis and can change
235
+ background subtraction and particle detections. It reduces raw and background
236
+ stack storage by about half, but not total process memory because processing
237
+ also uses float32 working arrays. For uint8 TIFF input, this option leaves the
238
+ pixel values unchanged.
239
+
240
+ Chunking reduces localization working-block size, but does not make the exact
241
+ stack-wide static-background calculation bounded-memory. When that
242
+ preprocessing option is enabled, the selected ROI stack must fit in RAM even
243
+ in chunked mode. Reduce the ROI or frame range if memory is insufficient.
244
+
245
+
246
+
247
+ See the [2.0 documentation](./NANOOBJECTDETECTION_2_0_DOCUMENTATION.md) for
248
+ the complete list of supported settings, generated files, assumptions, and
249
+ known limitations.