OpenCVSharp Frame Buffer Pooling
/ 3 min read
OpenCVSharp을 사용해 비디오의 매 프레임을 가져오는 기능을 개발했다.
private static void ReadEachFrame_Normal(){ string path = "path/top/your/video.mp4"; VideoCapture capture = new VideoCapture(path); capture.Open(path); using Mat frame = new Mat(); while (capture.Read(frame)) { byte[] bytes = frame.ToBytes(); }}OpenCVSharp 라이브러리에는 내가 아는한 쉽게 프레임 버퍼를 입력한 버퍼로 가져오거나, 풀링이 적용된 형태로 가져올 수 있는 방법이 없다.
Mat.ToBytes()로 새로 생성된 byte[] 객체를 반환받는 방법이 가장 일반적으로 보였다.
내가 사용하는 케이스에선 동시에 여러 비디오의 프레임을 가져오다보니
byte[]의 할당과 버려짐이 상당했다.
그로 인해 GC 압력이 증가하고, 잦은 GC Collect가 일어날 것이 자명했다.
따라서 풀링을 적용해보기 위해 구글링을 열심히 한 결과..
https://github.com/shimat/opencvsharp/issues/784 에서 힌트를 찾을 수 있었다.
위 방법과 적절한 캡슐화를 거쳐서 아래의 코드를 작성했다.
public struct PoolingBytes : IDisposable{ public PoolingBytes(IMemoryOwner<byte> memory, int offset, int length) { Memory = memory; Length = length; }
public IMemoryOwner<byte>? Memory { get; private set; } public int Offset { get; init; } public int Length { get; init; }
public void Dispose() { if (Memory != null) { Memory.Dispose(); Memory = null; } }
public ReadOnlySpan<byte> AsSpan() { return Memory!.Memory.Span.Slice(Offset, Length); }}
...
public static unsafe PoolingBytes GetBytesPooled(Mat mat, VectorOfByte bufferVec){ InputArray inputArray = mat; NativeMethods.imgcodecs_imencode_vector(".png", inputArray.CvPtr, bufferVec.CvPtr, null, 0, out int ret); var rentArray = MemoryPool<byte>.Shared.Rent(bufferVec.Size); using var pin = rentArray.Memory.Pin(); Buffer.MemoryCopy((void*)bufferVec.ElemPtr, (void*)pin.Pointer, bufferVec.Size, bufferVec.Size); return new PoolingBytes(rentArray, 0, bufferVec.Size);}MemoryPool<byte>.Shared.Rent()를 호출하면 입력한 파라미터보다 크거나 같은 크기의 메모리를 반환한다.
그렇기에 정확한 이미지 버퍼 영역을 지정하기 위해 추가적으로 Offset과 Length 프로퍼티를 사용한다.
Test Code:
[MemoryDiagnoser][SimpleJob(RunStrategy.ColdStart, launchCount: 1, iterationCount: 1000)]public class OpenCVPooling : IDisposable{ string path = "path/top/your/video.mp4";
private VideoCapture? capture; private Mat frame = new Mat(); private VectorOfByte bufferVec = new VectorOfByte();
public void Dispose() { frame?.Dispose(); bufferVec?.Dispose(); }
[IterationSetup] public void Setup() { capture?.Dispose(); capture = new VideoCapture(videoPath); capture.Read(frame); }
[Benchmark] public void Normal() { byte[] bytes = frame.ToBytes(); }
[Benchmark] public void Pooled() { using PoolingBytes bytes = OpenCVUtility.GetBytesPooled(frame, bufferVec); }BenchmarkDotNet으로 테스트한 결과이다.
소요 시간자체는 큰 차이가 없지만, Allocation을 거의 없앴다는 점을 주목해야 한다.
| Method | Mean | Error | StdDev | Median | Allocated |
|---|---|---|---|---|---|
| Normal | 18.66 ms | 0.053 ms | 0.511 ms | 18.57 ms | 1072120 B |
| Pooled | 17.88 ms | 0.044 ms | 0.423 ms | 17.76 ms | 840 B |