Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
01d73c2
Rewritten the main vision documentation page.
SimoneMartino98 May 29, 2025
ef9f5ae
Fixed the doc visualization for all the classes.
SimoneMartino98 May 30, 2025
a6b9529
Added selection keyword to the get_soap function.
SimoneMartino98 Jun 3, 2025
7ce69c5
Added vision gui tutorial gif.
SimoneMartino98 Jun 4, 2025
87bddd9
Added adaptive image rescaling in vision gui.
SimoneMartino98 Jun 4, 2025
0f0ae12
Added the initial track module.
SimoneMartino98 Jun 4, 2025
b3d74dc
Update dependencies for track.
SimoneMartino98 Jun 4, 2025
750c267
track_xyz returns Trj.
SimoneMartino98 Jun 4, 2025
487d2b4
Fixed vision GUI section in doc.
SimoneMartino98 Jun 4, 2025
97d0ee7
Improved doc content.
SimoneMartino98 Jun 5, 2025
a7ef277
Merge branch 'GMPavanLab:main' into vision_revision
SimoneMartino98 Jun 5, 2025
dbe0bc9
Added track block dependencies.
SimoneMartino98 Jun 5, 2025
acf2dd6
Update formatting.
SimoneMartino98 Jun 5, 2025
3ffc6bf
Added track block in Just conf file.
SimoneMartino98 Jun 5, 2025
d7e750f
Added track in readthedocs config file.
SimoneMartino98 Jun 5, 2025
db183c3
Updated vision documentation and figure.
SimoneMartino98 Jun 11, 2025
5af3e12
Added lens insight on video_detection example code.
SimoneMartino98 Jun 11, 2025
210d1c6
Commented the vision_gui code.
SimoneMartino98 Jun 11, 2025
4f9617e
Updated parameters track documentation.
SimoneMartino98 Jun 11, 2025
9267646
Added formatting spaces.
SimoneMartino98 Jun 13, 2025
8e9dba2
Added track adaptivity errors checks.
SimoneMartino98 Jun 13, 2025
2282d95
Removed track from dev dependencies.
SimoneMartino98 Jun 16, 2025
5582f33
Added Onion plot.
SimoneMartino98 Jun 16, 2025
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
6 changes: 3 additions & 3 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
- run: pip install -e '.[dev]'
- run: pip install -e '.[dev, track]'
- run: pytest --cov=src --cov-report term-missing
doctest:
runs-on: ubuntu-latest
Expand All @@ -74,5 +74,5 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
- run: pip install -e '.[dev]'
- run: make -C docs doctest
- run: pip install -e '.[dev, track]'
- run: make -C docs doctest
19 changes: 10 additions & 9 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
version: 2

build:
os: ubuntu-22.04
tools:
python: "3.10"
os: ubuntu-22.04
tools:
python: "3.10"

sphinx:
configuration: docs/source/conf.py
configuration: docs/source/conf.py

python:
install:
- method: pip
path: .
extra_requirements:
- dev
install:
- method: pip
path: .
extra_requirements:
- dev
- track
Binary file added docs/source/_static/vision/vision_gui.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/vision/vision_scheme.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 0 additions & 11 deletions docs/source/_templates/class.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,3 @@
{%- endfor %}
{% endif %}
{% endblock %}

{% block attributes %}
{% if attributes %}
.. rubric:: {{ _('Attributes') }}

.. autosummary::
{% for item in attributes %}
~{{ name }}.{{ item }}
{%- endfor %}
{% endif %}
{% endblock %}
3 changes: 2 additions & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
tICA <tica>
onion clustering <onion>
vision <vision>
track <track>
analysis <analysis>
data processing <data_processing>
HDF5er <hdf5er>
Expand Down Expand Up @@ -74,7 +75,7 @@ or with conda::
If you want to use the :mod:`dynsight.vision` and :mod:`dynsight.track` modules
you will need to install a series of packages. This can be done with with pip::

$ pip install ultralytics PyYAML
$ pip install ultralytics PyYAML pandas trackpy


How to contribute
Expand Down
14 changes: 14 additions & 0 deletions docs/source/track.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
track
=====

This module tracks particle trajectories in `.xyz` files that lack explicit
particle IDs, assigning consistent identities across frames.
It is especially useful for outputs generated by the :doc:`vision` module.

Functions
---------

.. toctree::
:maxdepth: 1

track_xyz <_autosummary/dynsight.track.track_xyz>
41 changes: 40 additions & 1 deletion docs/source/vision.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,43 @@
Vision
======

Code for computer vision and trajectory extraction from experimental videos.
Code that contains useful tools to obtain trajectories from video files.
The extraction is possible thanks to the optimization of a Convolutional Neural
Network (CNN) model training to
detect specific objects in the video. Once the positions are recovered, a
tracking algorithm (:doc:`track`) links each object with its own identity number.

.. figure:: _static/vision/vision_scheme.jpeg
:alt: Schematic representation of a typical dynsight.vision application.
:align: center

Schematic representation of a typical `dynsight.vision` application.
Comment thread
andrewtarzia marked this conversation as resolved.

The object detection is managed by the Ultralytics_ external library. If you
use this tool, please cite them by referring to this link_.

.. _Ultralytics: https://www.ultralytics.com
.. _link: https://docs.ultralytics.com/models/yolo11/#citations-and-acknowledgements

--------------
The Vision GUI
--------------

The `dynsight.vision` module includes an internal Graphical User Interface (GUI)
designed to assist users in preparing the training items required for object
detection. It only requires an *initial guess* of the objects present in
the video, which is then used to generate a synthetic dataset to bootstrap the
training process.

.. tip::

Applying some basic color correction to the input video (especially converting
it to grayscale with enhanced contrast) can significantly improve the quality
of the initial model.

.. image:: _static/vision/vision_gui.gif
:alt: Usage of the GUI.
:align: center

-----
Usage
Expand All @@ -12,3 +48,6 @@ Usage

Video <_autosummary/dynsight.vision.Video.rst>
Detect <_autosummary/dynsight.vision.Detect.rst>

Once obtained the particle positions, is possible to obtain the trajectories of
each particle by using the :doc:`track` module.
15 changes: 15 additions & 0 deletions examples/video_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,25 @@ def main() -> None:
)
if args.predict:
detection.predict_frames(model_path=args.detect_model)
xyz_trajectory = Path.cwd() / "trajectory.xyz"
detection.compute_xyz(
prediction_folder_path=Path("prediction"),
output_path=Path.cwd(),
)
tr_xyz_trajectory = Path.cwd() / "tracked_trajectory.xyz"
trj = dynsight.track.track_xyz(
input_xyz=xyz_trajectory,
output_xyz=tr_xyz_trajectory,
)
# Usage example of the trj object after tracking.
lens_descriptor = trj.get_lens(r_cut=5)
lens_descriptor.dump_to_json(output_project)
Comment thread
andrewtarzia marked this conversation as resolved.
# Compute clustering
onion_lens = lens_descriptor.get_onion(delta_t=2)
onion_lens.plot_output(
file_path=args.output,
data_insight=lens_descriptor,
)

Comment thread
andrewtarzia marked this conversation as resolved.

if __name__ == "__main__":
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dev = [
"furo",
"numba",
]
track = ["trackpy", "pandas"]

[project.urls]
github = "https://github.com/GMPavanLab/dynsight"
Expand Down Expand Up @@ -110,6 +111,8 @@ module = [
'Pillow.*',
'ultralytics.*',
'opencv-python.*',
'pandas.*',
'trackpy.*',
'cv2.*',
]
ignore_missing_imports = true
2 changes: 2 additions & 0 deletions src/dynsight/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
onion,
soap,
tica,
track,
trajectory,
utilities,
vision,
Expand All @@ -21,6 +22,7 @@
"onion",
"soap",
"tica",
"track",
"trajectory",
"utilities",
"vision",
Expand Down
Empty file.
138 changes: 138 additions & 0 deletions src/dynsight/_internal/track/track.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
from __future__ import annotations

import logging
from pathlib import Path

import pandas as pd
import trackpy as tp

from dynsight.trajectory import Trj

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
)
logger = logging.getLogger(__name__)


def track_xyz(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some interesting parameters here, which probably need a bit more of an explanation of their ranges and their meaning. I see your doc string, but I think it needs more.

For all these:

  • search_range: float = 10,
  • memory: int = 1,
  • adaptive_step: float = 0.5,
  • adaptive_stop: float = 0.95,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How big can each be? What are appropriate ranges?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took the doc from the original trackpy documentation.
There aren't appropriate ranges for these. They depend on the system size.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So how does the user select what values to use?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trial and error, based on the system they have. (concentration of particles, velocity of them, vibrations).

input_xyz: Path,
output_xyz: Path,
search_range: float = 10,
memory: int = 1,
adaptive_stop: None | float = 0.95,
adaptive_step: None | float = 0.5,
) -> Trj:
"""Track particles from an `.xyz` file and write a new file with IDs.

The input `.xyz` is assumed to contain only raw 3D coordinates
(without atom labels/identity), and each frame begins with a line
indicating the number of atoms, followed by a comment line, then a list of
positions. Each frame in the input file must follow this structure::

<number of atoms>
comment line
<x> <y> <z>
<x> <y> <z>
...
<x> <y> <z>

The output file will have the same structure, but each line will start
with the tracked particle ID.

Parameters:
input_xyz:
Path to the input .xyz file containing positions only.

output_xyz:
Path where the output .xyz file with particle IDs will be saved.

search_range:
The maximum allowable displacement of objects between frames for
them to be considered the same particle.

memory:
The maximum number of frames during which an object can vanish,
then reppear nearby, and be considered the same particle.

adaptive_stop:
Comment thread
andrewtarzia marked this conversation as resolved.
If not `None`, when encountering a region with too many candidate
links (subnet), retry by progressively reducing `search_range`
until the subnet is solvable. If `search_range` becomes less or
equal than the `adaptive_stop`, give up and raise a
`SubnetOversizeException`.

adaptive_step:
Factor by which the `search_range` is multiplied to reduce it
during adaptive search. Effective only if `adaptive_stop` is not
`None`.
Comment thread
andrewtarzia marked this conversation as resolved.
"""
if adaptive_stop is None and adaptive_step is not None:
msg = "adaptive_step is set but adaptive_stop is None."
raise ValueError(msg)
if adaptive_stop is not None and adaptive_step is None:
msg = "adaptive_stop is set but adaptive_step is None."
raise ValueError(msg)

input_xyz = Path(input_xyz)
output_xyz = Path(output_xyz)

if not input_xyz.exists():
msg = f"Input file not found: {input_xyz}"
raise FileNotFoundError(msg)

positions = _collect_positions(input_xyz=input_xyz)

if not {"frame", "x", "y", "z"}.issubset(positions.columns):
msg = "Error in the .xyz format. Each line must be <x> <y> <z>."
raise ValueError(msg)

linked = tp.link_df(
positions,
search_range=search_range,
memory=memory,
adaptive_step=adaptive_step,
adaptive_stop=adaptive_stop,
)

with output_xyz.open("w") as f:
for frame_num in sorted(linked["frame"].unique()):
frame_data = linked[linked["frame"] == frame_num]
f.write(f"{len(frame_data)}\n")
f.write(f"Frame {frame_num}\n")
for _, row in frame_data.iterrows():
pid = int(row["particle"])
x, y, z = row["x"], row["y"], row["z"]
f.write(f"{pid} {x:.6f} {y:.6f} {z:.6f}\n")

logger.info(f"Linked .xyz file written to: {output_xyz}")
return Trj.init_from_xyz(traj_file=output_xyz, dt=1)


def _collect_positions(input_xyz: Path) -> pd.DataFrame:
"""Read the xyz file and return the positions dataset at each frame."""
lines = input_xyz.read_text().splitlines()
Comment thread
andrewtarzia marked this conversation as resolved.

data = []
frame = -1
row = 0
dimensions = 3
for _ in range(len(lines)):
Comment thread
andrewtarzia marked this conversation as resolved.
if row >= len(lines):
break
if lines[row].strip().isdigit():
num_atoms = int(lines[row])
frame += 1
row += 2 # skip comment line.
for a in range(num_atoms):
if row + a >= len(lines):
break
parts = lines[row + a].strip().split()
if len(parts) >= dimensions:
x, y, z = map(float, parts[0:3])
data.append({"frame": frame, "x": x, "y": y, "z": z})
row += num_atoms
else:
row += 1

return pd.DataFrame(data)
Loading