Skip to content

Commit f491bcb

Browse files
shimatclaude
andcommitted
Add AsRows<T>/RowSpan<T> for efficient pixel access; fix Row() GC bug; obsolete Marshal-based indexer
- Fix missing GC.KeepAlive(this) in Mat.Row() (present in RowRange but absent in Row) - Add Mat.RowSpan<T>(int row): returns a Span<T> over a single row without allocating a submatrix object, avoiding P/Invoke per element - Add MatRowAccessor<T> (ref struct) and Mat.AsRows<T>(): captures data pointer and step once at construction, then all row/element access is pure pointer arithmetic with zero P/Invoke per row or element — the recommended API for high-performance pixel loops - Add #if DEBUG guard in At<T>(int i0) to detect misuse on 1-row 2D submatrices (e.g. Row(i).At<T>(col) silently accesses wrong memory); guard is compile-time only so Release builds have no overhead - Mark Mat.Indexer<T> and GetGenericIndexer<T>() as [Obsolete]: Marshal.PtrToStructure is slower than At<T> and AsRows<T> and predates the unmanaged constraint Closes #1775 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ec06732 commit f491bcb

2 files changed

Lines changed: 118 additions & 3 deletions

File tree

src/OpenCvSharp/Modules/core/Mat/Mat.cs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,7 @@ public Mat Row(int y)
15521552
ThrowIfDisposed();
15531553
NativeMethods.HandleException(
15541554
NativeMethods.core_Mat_row(CvPtr, y, out var matPtr));
1555+
GC.KeepAlive(this);
15551556
return new Mat(matPtr);
15561557
}
15571558

@@ -3118,6 +3119,8 @@ public Mat EmptyClone()
31183119
/// </summary>
31193120
/// <typeparam name="T"></typeparam>
31203121
/// <returns></returns>
3122+
[Obsolete("Use At<T>(row, col) for occasional access, or AsRows<T>() for high-performance loops. " +
3123+
"GetGenericIndexer uses Marshal.PtrToStructure which is slower than both alternatives.")]
31213124
public Indexer<T> GetGenericIndexer<T>() where T : struct
31223125
{
31233126
return new Indexer<T>(this);
@@ -3136,6 +3139,8 @@ public UnsafeIndexer<T> GetUnsafeGenericIndexer<T>() where T : unmanaged
31363139
/// Mat Indexer
31373140
/// </summary>
31383141
/// <typeparam name="T"></typeparam>
3142+
[Obsolete("Use At<T>(row, col) for occasional access, or AsRows<T>() for high-performance loops. " +
3143+
"This class uses Marshal.PtrToStructure which is slower than both alternatives.")]
31393144
public sealed class Indexer<T> : MatIndexer<T> where T : struct
31403145
{
31413146
private readonly long ptrVal;
@@ -3401,13 +3406,23 @@ public T Get<T>(params int[] idx) where T : struct
34013406
}
34023407

34033408
/// <summary>
3404-
/// Returns a value to the specified array element.
3409+
/// Returns a reference to the specified array element.
3410+
/// For 2D matrices, <paramref name="i0"/> is the row index (dimension 0).
3411+
/// Do NOT call this with a column index on a row-submatrix obtained from <see cref="Row"/>;
3412+
/// use <see cref="At{T}(int, int)"/> or <see cref="RowSpan{T}"/> instead.
3413+
/// For performance-sensitive pixel loops, prefer <see cref="AsRows{T}"/>.
34053414
/// </summary>
34063415
/// <typeparam name="T"></typeparam>
34073416
/// <param name="i0">Index along the dimension 0</param>
3408-
/// <returns>A value to the specified array element.</returns>
3417+
/// <returns>A reference to the specified array element.</returns>
34093418
public unsafe ref T At<T>(int i0) where T : unmanaged
34103419
{
3420+
#if DEBUG
3421+
if (Dims == 2 && Rows == 1 && Cols > 1)
3422+
throw new InvalidOperationException(
3423+
"At<T>(int) on a 1-row 2D matrix treats i0 as a row index, not a column index. " +
3424+
"Use At<T>(0, col) or RowSpan<T>(0) instead.");
3425+
#endif
34113426
var p = Ptr(i0);
34123427
return ref Unsafe.AsRef<T>(p.ToPointer());
34133428
}
@@ -4241,12 +4256,49 @@ public void ForEachAsVec6d(MatForeachFunctionVec6d operation)
42414256

42424257
#endregion
42434258

4259+
/// <summary>
4260+
/// Returns a <see cref="Span{T}"/> over a single row of this matrix without allocating a submatrix object.
4261+
/// Padding bytes between rows are excluded from the span.
4262+
/// For iterating all rows in a loop, prefer <see cref="AsRows{T}"/> which captures the step once.
4263+
/// </summary>
4264+
/// <typeparam name="T">Element type. Must match the matrix element type.</typeparam>
4265+
/// <param name="row">Zero-based row index.</param>
4266+
/// <returns>A span covering the <paramref name="row"/>-th row.</returns>
4267+
public unsafe Span<T> RowSpan<T>(int row) where T : unmanaged
4268+
{
4269+
ThrowIfDisposed();
4270+
if (Dims != 2)
4271+
throw new InvalidOperationException("RowSpan is only supported for 2D matrices.");
4272+
if ((uint)row >= (uint)Rows)
4273+
throw new ArgumentOutOfRangeException(nameof(row));
4274+
var rowPtr = DataPointer + (nint)Step(0) * row;
4275+
return new Span<T>(rowPtr, Cols * ElemSize() / sizeof(T));
4276+
}
4277+
4278+
/// <summary>
4279+
/// Returns a <see cref="MatRowAccessor{T}"/> that provides efficient row-by-row access
4280+
/// with no P/Invoke per row or per element.
4281+
/// The data pointer, step, and dimensions are captured once; all indexing is pure pointer arithmetic.
4282+
/// </summary>
4283+
/// <typeparam name="T">Element type. Must match the matrix element type (e.g. <see cref="Vec3b"/> for CV_8UC3).</typeparam>
4284+
/// <returns>A <see cref="MatRowAccessor{T}"/> over this matrix.</returns>
4285+
/// <exception cref="InvalidOperationException">Thrown when the matrix is not 2-dimensional.</exception>
4286+
public unsafe MatRowAccessor<T> AsRows<T>() where T : unmanaged
4287+
{
4288+
ThrowIfDisposed();
4289+
if (Dims != 2)
4290+
throw new InvalidOperationException("AsRows is only supported for 2D matrices.");
4291+
var elemCount = Cols * ElemSize() / sizeof(T);
4292+
return new MatRowAccessor<T>((nint)DataPointer, (nint)Step(0), Rows, elemCount);
4293+
}
4294+
42444295
/// <summary>
42454296
/// Creates a new span over the Mat.
4297+
/// The matrix must be continuous (<see cref="IsContinuous"/>); returns an empty span otherwise.
42464298
/// </summary>
42474299
/// <typeparam name="T"></typeparam>
42484300
/// <returns></returns>
4249-
public unsafe Span<T> AsSpan<T>() where T : unmanaged
4301+
public unsafe Span<T> AsSpan<T>() where T : unmanaged
42504302
=> IsContinuous() ? new Span<T>(DataPointer, (int)Total() * ElemSize() / sizeof(T)) : [];
42514303

42524304
#endregion
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
namespace OpenCvSharp;
2+
3+
/// <summary>
4+
/// Provides zero-allocation, zero-P/Invoke-per-element row access to a 2D <see cref="Mat"/>.
5+
/// Obtain via <see cref="Mat.AsRows{T}"/>. The data pointer, step, and dimensions are captured
6+
/// once at construction time, so all subsequent row indexing is pure pointer arithmetic.
7+
/// </summary>
8+
/// <typeparam name="T">Unmanaged element type matching the matrix element type (e.g. <see cref="Vec3b"/> for CV_8UC3).</typeparam>
9+
/// <remarks>
10+
/// <para>
11+
/// Typical usage for per-pixel iteration:
12+
/// <code>
13+
/// var rows = mat.AsRows&lt;Vec3b&gt;();
14+
/// for (int r = 0; r &lt; rows.Count; r++)
15+
/// {
16+
/// Span&lt;Vec3b&gt; row = rows[r];
17+
/// for (int c = 0; c &lt; row.Length; c++)
18+
/// {
19+
/// Vec3b pixel = row[c];
20+
/// }
21+
/// }
22+
/// </code>
23+
/// </para>
24+
/// <para>
25+
/// This is a <c>ref struct</c> and therefore cannot be stored in fields, boxed, or used as a
26+
/// generic type argument.
27+
/// </para>
28+
/// </remarks>
29+
public readonly ref struct MatRowAccessor<T> where T : unmanaged
30+
{
31+
private readonly nint _data;
32+
private readonly nint _step;
33+
private readonly int _cols;
34+
35+
/// <summary>Number of rows in the matrix.</summary>
36+
public int Count { get; }
37+
38+
internal MatRowAccessor(nint data, nint step, int rows, int cols)
39+
{
40+
_data = data;
41+
_step = step;
42+
Count = rows;
43+
_cols = cols;
44+
}
45+
46+
/// <summary>
47+
/// Returns a <see cref="Span{T}"/> over the specified row.
48+
/// No P/Invoke is performed; this is pure pointer arithmetic.
49+
/// Bounds are checked in DEBUG builds only.
50+
/// </summary>
51+
/// <param name="row">Zero-based row index.</param>
52+
public unsafe Span<T> this[int row]
53+
{
54+
get
55+
{
56+
#if DEBUG
57+
if ((uint)row >= (uint)Count)
58+
throw new ArgumentOutOfRangeException(nameof(row));
59+
#endif
60+
return new Span<T>((void*)(_data + _step * row), _cols);
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)