Repository navigation
Revisit pixel grid definitions and geometry - #679
wesselvannierop wants to merge 41 commits into
Conversation
- Updated compute_scan_convert_2d_coordinates and scan_convert_2d functions to replace distance_to_apex with grid_apex and extent parameters. - Introduced a helper function _deprecated_distance_to_apex to handle legacy distance_to_apex usage and log deprecation warnings. - Modified Parameters class to derive grid_apex from probe geometry and transmit origins, removing distance_to_apex as a parameter. - Adjusted related methods and documentation to reflect the new parameter structure and clarify the role of grid_apex in polar grid calculations. - Updated fit_curved_probe_radius to align with the new grid_apex definition.
- Renamed rho_range to rlims and theta_range to polar_limits for 2D scan conversion, matching the beamforming grid's names. In Parameters, scan conversion now reads the grid's own polar_limits instead of a copy. - Renamed the 3D frustum angles to lateral_tilt_limits (atan(x/z)) and elevation_tilt_limits (atan(y/z)), since they are not spherical polar and azimuth angles. scan_convert falls back to polar_limits for the lateral tilt. - Kept rho_range, theta_range and phi_range as deprecated aliases in the display functions, plot_frustum_vertices, the ScanConvert op and Parameters. - Parameters.coordinates_2d now treats only an explicit rlims as an external polar image. - Renamed the ScanConvert output keys to the new names. - Updated configs, notebooks, the pixel grid explorer and figure script, and added an angle and radius naming table to the config docs.
…rray - transmit_apex_distance now finds an apex only from steered focused transmits. Steered plane and diverging waves do not fan out from their origin, so their axes no longer place grid_apex at the array middle. - On a flat array without a transmit apex, polar_limits is measured from the virtual apex where the outermost edge ray, traced back from its array end, crosses the central axis, which is where grid_apex places it. The edge rays' start points now also bound the sector, so it covers the insonified region from the array down instead of only at zlims[1]. - Updated the tests: steered plane waves now give a sector along their outer edges, and steered diverging waves define no apex at the array middle.
- Ported the new apex rule for steered plane and diverging waves to the explorer and rebuilt the docs page. - The explorer now fills the window on wide screens: only the sidebar scrolls, and the figure is sized to the space left. - Trimmed the presets (no scanline grids, no single diverging wave). The phased docs figure now spreads its transmit origins over the array and uses the default zlims. - Removed the static pixel grid SVGs and their figure script; the config docs embed the explorer instead.
- Scanline polar_limits follow the transmit order; uneven angles raise - grid_apex uses the apex the derived polar_limits are seen from - Deprecated scan conversion inputs override the derived values in ScanConvert - Overridden rlims/polar_limits drop the derived grid_apex and extent - distance_to_apex keeps its positional slot; grid_apex/extent are keyword-only - A distance_to_apex parameter is ignored with a deprecation warning - Cache the transmit apex, edge rays and derived sector; skip private properties - Move deprecated_alias to zea.internal.utils
Build docs/_static/pixel-grid-explorer.html from docs/explorer in a builder-inited hook instead of committing it, so it cannot go stale. This adds about a second to the docs build.
…tests Copies the helpers from #673 into zea.beamform.geometry and uses them in the grid geometry, transmit selection, scanline check and focal-region warning.
A scanline grid's polar_limits are its transmit angles, so grid_apex should not take the apex the derived sector is seen from. Matches the pixel grid explorer.
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
Still working on fully fixing #47 |
…ions Grid and scan-conversion angles are rlims, polar_limits (atan(x/z)) and, in 3D, elevation_limits (atan(y/z)), with polar_margin and elevation_margin. Transmit angles stay spherical (polar_angles, azimuth_angles; physics convention). - Replace lateral_tilt_limits and elevation_tilt_limits with polar_limits and elevation_limits; ScanConvert outputs d_polar and d_elevation. - Derive elevation_limits (and ylims) from the transmits' elevation angle; azimuth_limits becomes a deprecated alias. Add elevation_margin. - Rename theta_range to polar_limits in the polar/cartesian display helpers. - Add an angle conventions section to the docs and a names card to the pixel grid explorer; clarify the spec descriptions of polar_angles and azimuth_angles.
|
Fixed in this PR (bb5da16): the Verasonics converter stored elevation steering as This PR documents The converter now converts
Open-H-RF is unaffected. |
The FIELD_METADATA descriptions of polar_angles and azimuth_angles, shown in the spec reference table, now match the class docstring word for word.
The deprecation warning already names the replacement.
The test was skipped in CI because quickjs, which is archived, was never installed. It now runs the explorer's JavaScript in Node.js, skipping when node is missing unless ZEA_REQUIRE_NODE is set. The tests job sets up Node.js 24 and sets ZEA_REQUIRE_NODE.
The scanline grid, its polar_limits and xlims used the raw spherical polar angles, while every other default projects them onto the x-z plane. Directions stored as theta >= 0 with phi = pi then mirrored the scanline columns. They now use the projected angles too, and the cartesian scanline grid no longer gets azimuth_angles, since its angles are already projected.
…e explorer zea uses the first and last transmit angle, since scan conversion places the columns in transmit order; the explorer used the min and max. The explorer only generates ascending angles, so what it shows does not change.
…ctions - cartesian_to_polar_matrix, polar_to_cartesian_matrix, polar_to_cartesian_coordinates and inverse_scan_convert_2d measured angles positive toward -x. polar_limits now measures them toward +x with columns from polar_limits[0] to polar_limits[1], as in scan_convert_2d, so the inverse takes the same polar_limits as the forward conversion. The deprecated theta_range alias keeps its old meaning by mirroring the values. - frustum_convert_rtp2xyz, rt2xz, xz2rt and xyz2rtp are deprecated in favour of frustum_to_xyz, frustum_to_xz, xz_to_frustum and xyz_to_frustum, which take radius, polar and elevation, so theta and phi only name the spherical transmit angles. - plot_frustum_vertices takes elevation_plane, polar_plane and radius_plane (and their styles); the phi, theta and rho names are deprecated aliases. - Docstrings name the grid angles atan(x / z) and atan(y / z) instead of theta and phi.
- The default scan-conversion resolution uses the absolute angle steps, so descending polar_limits (a scanline grid steered from +x to -x) no longer give a negative resolution and an empty image. - prepare_parameters also drops the precomputed coordinates when the limits or the resolution are overridden; ScanConvert used them, so the override had no effect on the image. - ScanConvert maps the deprecated distance_to_apex input onto grid_apex instead of ignoring it. - virtual_apex_distance replaces the two copies of the virtual-apex formula in grid_apex and _derived_polar_sector, and ignores sectors within STEERING_TOLERANCE of 0, which put the apex kilometres behind the array. - deprecated_distance_to_apex moves to zea.internal.utils, so polar_pixel_grid and ScanConvert share it with the display functions. - The Verasonics converter reads and converts the steering angles once.
- cartesian_to_polar_matrix, polar_to_cartesian_matrix, polar_to_cartesian_coordinates and inverse_scan_convert_2d take theta_range in the position it used to have, and polar_limits as a keyword only. Code that passed the angles by position goes through the mirroring shim with a deprecation warning, instead of getting a mirrored image without one. - get_properties_with_dependencies includes private properties, so a misspelled dependency of e.g. _edge_rays is caught when the class is instantiated. get_properties still leaves them out, so to_tensor is unchanged.
The derived rlims are measured from grid_apex, so dropping the apex when only polar_limits or elevation_limits were overridden shifted the depth axis by the apex distance. Move the override logic into _stale_derived_keys.
Explicit rlims on Parameters describe a polar image of their own, which Parameters.coordinates_2d already accounts for. When ScanConvert recomputed the coordinates (e.g. for a resolution override) it still got the grid's apex and output window, which cropped or shifted the image.
elevation_limits reads azimuth_angles but did not declare it as a dependency, so setting azimuth_angles after elevation_limits was read left it stale.
The 3D masking and indexing assume ascending polar and elevation limits, so descending ones masked out the whole volume. Assert ascending limits, like rlims, instead of claiming either order works.
f_number repeated the 90%-of-pitch rule from _resolve_element_width with a different pitch estimator. _resolve_element_width now measures the pitch along the array with element_pitch, which also works for curved arrays, and f_number uses it.
…an apex transmit_apex_distance only checked the steered focused axes, so an unsteered focused transmit off the central axis was accepted, and a polar scanline grid drew its line on the central axis instead of its own.
ylims read grid_apex unconditionally, so every 2D grid fitted the transmit apex and derived the polar sector (warning about missing focus distances) for an elevation term that is zero.
The crop also removes pixels in front of the array at the sector edges, not only those a virtual apex places behind it. That is intended: zlims[0] is where the image starts. A test now covers a set zlims[0] for both apex types.
…ions # Conflicts: # zea/beamform/pixelgrid.py
_resolve_element_width measures the pitch with element_pitch since e979240, but test_pfield_infers_element_width still built its expected width from Probe.get_pitch. On float32 geometry the two differ by about 4e-7 relative, which is enough to change the pfield on TensorFlow.
…ions # Conflicts: # docs/source/changelog.rst # docs/source/notebooks/data/zea_data_example.ipynb # zea/beamform/beamformer.py # zea/beamform/pixelgrid.py # zea/parameters.py
This PR fixes the various names we have been using for angles and coordinate systems, and sticks to one convention. Also adds an interactive beamforming grid viewer, which shows visually how all the variables relate.
Also derives the default pixel grid from the probe and transmit geometry, so polar sectors, their apex and the cartesian width follow what the transmits actually insonify, and settles the angle names.
Fixes #669 and #47 (angle naming); touches #547 (acquisition-dependent defaults).
Changes
grid_apex(new, derived) replacesdistance_to_apex: the point the polar grid fans out from, i.e. where steered focused axes meet, a diverging wave's virtual source, or a curved array's centre of curvature.polar_limitscover the region each transmit insonifies atzlims[1], replacing the fixed 15 % margin. Newpolar_marginsets a fixed margin instead.xlims,grid_size_x,extentfollow the sector or insonified width (λ/4 along the outer arc), and scan conversion fillsextent.grid_apex; unsupported transmit/grid combinations raise aValueError.f_numberis derived from the element's −6 dB directivity (0, no receive mask, for typical phased arrays). This changes pixel values, not the grid.rlims(radius fromgrid_apex),polar_limits(atan(x/z)), and in 3D one more,elevation_limits(atan(y/z)). θ and φ only name the transmit angles as the file format stores them: sphericalpolar_anglesandazimuth_angles, physics convention.elevation_limits(and so the defaultylims) come from the transmits' elevation angleatan(tanθ·sinφ), instead of the range of φ. Newelevation_marginsets a fixed margin, likepolar_margin.polar_anglesandazimuth_anglesare now the spherical transmit angles, converted fromTX.Steer.azimuth_anglesused to hold the elevation steering anglealpha, soSteer = (0, 10°)was read as θ = 0, φ = 10°, which points straight down; it is now θ = 10°, φ = 90°. Data withalpha = 0converts as before; files already converted from elevation-steered acquisitions need to be reconverted (details).Breaking / deprecated
rho_range/theta_range/phi_range→rlims/polar_limits/elevation_limits. Deprecated aliases kept. The polar/cartesian helpers now measure angles toward +x, like the rest of zea (theta_rangestill gives the old images).frustum_convert_*→frustum_to_xyz/xyz_to_frustum/frustum_to_xz/xz_to_frustum, andplot_frustum_vertices'phi_/theta_/rho_planes and styles →elevation_/polar_/radius_. Deprecated aliases kept.azimuth_limits→elevation_limits(deprecated alias). It already setylimsas an elevation angle, so explicit values keep their effect; the derived default changes (see above).ScanConvertoutputs are renamed without aliases (rho_range/theta_range/phi_range/d_rho/d_theta/d_phi→rlims/polar_limits/elevation_limits/dr/d_polar/d_elevation), so code reading the old keys breaks.f_numberchange, so beamformed images change.Overlaps with #673: this branch copies its
is_plane_wave/is_focused/is_diverginghelpers.Before / after grids
Curved, 1 plane wave
maingavepolar_limits[0°, 0°], so a zero-width, empty image. An unsteered plane wave on a curved array spreads along the element normals, so the sector is now the array's own angular span.Phased, scanline, virtual apex
maindisplayed lines fired at ±45° at ±58.5°, so every target was misplaced. Now each column follows its transmit axis and fans out from where the axes meet, 9.4 mm behind the array. The targets now land within about 0.8 mm, one pixel of the coarse 33-line image.Phased, plane waves
Steered plane waves cover a trapezoid: as wide as the array at the top, with edges at the steepest steering angle. The sector now runs through the array's ends at ±20°, from a virtual apex 26 mm back, so the shallow region under the array's ends is no longer cut off.
Phased, diverging waves
The old ±39° was the steering range plus a fixed 15 %, which cut off about 18 mm per side at 100 mm depth. The limits now follow the rays from each virtual source through the array's ends. The extra targets only show with the new
f_number, so both changes are needed.Phased, focused lines from a virtual apex
The old 15 % margin added 13.5° per side that no beam reached. The grid now fans out from where the lines come from: a virtual apex 9.4 mm behind the array. On
main, the 705 columns sampled the deepest row at only ~λ/2; the column count now follows the arc (λ/4). The grid change alone finds the same targets; the gain comes from the newf_number.Curved, focused lines along normals
The sector now ends at the outermost lines (±30°). The 8 targets not shown lie at 31–37°, in the outer tail of the outermost beams, 8–17 dB below the interior. Set
polar_marginto include that tail.Linear, plane waves (cartesian)
The old width came from an f-number-1 receive cone, regardless of the transmits. It now follows where the ±10° plane waves reach. The two targets dropped were at −44 dB on
main, at noise level.Curved, docs figure (explicit sector)
The column count now comes from the arc length at the deepest row (λ/4) instead of an unrelated
xlims. That removes about 20 % oversampling.