nn.FaceRecognizer.recognize() never frees the face detector result: leaks 32 bytes per call plus about 200 bytes per detected face
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 76/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Domain
- computer-vision, embedded-iot
Research direction
Start with components/nn/include/maix_nn_face_recognizer.hpp and inspect FaceRecognizer::recognize() alongside the detector detect() ownership notes. Check both the normal return path and the early return when feature inference yields no outputs; the issue includes a reproduction script using mallinfo2() to measure the leak. Done means detector results and the allocated faces object are cleaned up on every return path, with repeated calls no longer growing heap use.
Written by the indexing model from the issue text.
Description
中文摘要:nn.FaceRecognizer.recognize() 每次调用都不释放人脸检测器返回的结果(objs2 / objs):画面里没有脸时每次漏 32 字节(空结果容器),每检出一张脸再多漏约 200 字节。长时间运行、频繁认脸的应用内存会一直上涨:我们的应用每 0.5 秒认一次脸,连续运行 12 天内存涨了约 80 MB。修复只需在 recognize() 返回前释放检测结果,下文附复现脚本、源码位置与补丁。
Environment
- Device: MaixCAM2 (AX630C), factory image
- MaixPy 4.12.5 (latest release at the time of writing), glibc 2.35
- MaixCDK master:
components/nn/include/maix_nn_face_recognizer.hpp(last changed 2025-05-30) has the same code
Symptom
A long-running MaixPy app that calls FaceRecognizer.recognize() every 0.5 s grew by about 10 MB per day and by about 80 MB over 12 days of continuous running (excluding one-time allocations such as model loading).
Minimal reproduction
The script counts bytes allocated by malloc and not yet freed (mallinfo2()), which is exact. RSS is not a reliable measure for leaks of this size: small leaked chunks first fill free space that is already resident, and transparent huge pages make RSS jump in 2 MB steps.
import ctypes
import sys
from maix import image, nn
class MallInfo2(ctypes.Structure):
_fields_ = [(n, ctypes.c_size_t) for n in
("arena", "ordblks", "smblks", "hblks", "hblkhd", "usmblks", "fsmblks", "uordblks", "fordblks", "keepcost")]
libc = ctypes.CDLL("libc.so.6")
libc.mallinfo2.restype = MallInfo2
def heap_in_use():
m = libc.mallinfo2()
return m.uordblks + m.hblkhd
detect_model = sys.argv[1] if len(sys.argv) > 1 else "/root/models/yolo11s_face.mud"
fr = nn.FaceRecognizer(detect_model=detect_model,
feature_model="/root/models/insghtface_webface_r50.mud", dual_buff=False)
img = image.load("/root/one_face.jpg") # any photo with exactly one face
blank = image.Image(img.width(), img.height(), image.Format.FMT_RGB888)
blank.draw_rect(0, 0, blank.width(), blank.height(), image.Color.from_rgb(128, 128, 128), thickness=-1)
print("faces found: photo", len(fr.recognize(img, 0.5, 0.45, 0.8, False, False)),
"blank", len(fr.recognize(blank, 0.5, 0.45, 0.8, False, False)))
for im in (img, blank):
for _ in range(200): # warm up both paths first
fr.recognize(im, 0.5, 0.45, 0.8, False, False)
for name, im in (("no face", blank), ("one face", img)):
marks = [heap_in_use()]
for i in range(1, 1001):
fr.recognize(im, 0.5, 0.45, 0.8, False, False)
if i % 250 == 0:
marks.append(heap_in_use())
steps = [b - a for a, b in zip(marks, marks[1:])]
print(f"{name}: heap in use grows by {steps} bytes per 250 calls -> {(marks[-1] - marks[0]) / 1000:.1f} bytes per call")
Output on our device, with the two face detection models shipped in /root/models and one custom YOLOv8 face model:
/root/models/yolo11s_face.mud
faces found: photo 1 blank 0
no face: heap in use grows by [8000, 8000, 8000, 8000] bytes per 250 calls -> 32.0 bytes per call
one face: heap in use grows by [57216, 56064, 56528, 56128] bytes per 250 calls -> 225.9 bytes per call
/root/models/yolov8n_face.mud
faces found: photo 1 blank 0
no face: heap in use grows by [8000, 7968, 8032, 8000] bytes per 250 calls -> 32.0 bytes per call
one face: heap in use grows by [57104, 56096, 56016, 55872] bytes per 250 calls -> 225.1 bytes per call
custom yolov8 face model (1024x576 input)
faces found: photo 1 blank 0
no face: heap in use grows by [8000, 8000, 8000, 8000] bytes per 250 calls -> 32.0 bytes per call
one face: heap in use grows by [57312, 58000, 57952, 57952] bytes per 250 calls -> 231.2 bytes per call
The growth is linear. nn.YOLO11.detect() and nn.YOLOv8.detect() called directly from Python do not leak (the Python binding owns and deletes the returned Objects), so the leak is specific to FaceRecognizer.recognize().
Cause
In FaceRecognizer::recognize() the detector result is allocated by detect() ("In C++, you should delete it after use"), but it is never deleted:
std::vector<nn::Object> *objs;
nn::Objects *objs2 = nullptr;
if(_facedetector)
objs = _facedetector->detect(img, _conf_th, _iou_th, fit);
else if (_facedetector_retina)
objs = _facedetector_retina->detect(img, _conf_th, _iou_th, fit);
else if (_facedetector_yolov8)
objs2 = _facedetector_yolov8->detect(img, _conf_th, _iou_th, fit);
FaceObjects *faces = new nn::FaceObjects();
...
return faces; // objs / objs2 are not deleted
YOLO11::detect() returns a newly allocated Objects even when nothing is detected, which is the 32 bytes per call; each detected face adds one Object and its landmark vector. The early return inside the loop (if (!outputs) ... return new FaceObjects();) also leaks objs / objs2, and faces.
Suggested fix
tensor::Tensors *outputs = _model_feature->forward_image(*std_img, this->mean_feature, this->scale_feature, fit, false, true);
if (!outputs) // not ready for dual_buff mode
{
delete std_img;
+ delete faces;
+ if (objs2) delete objs2; else delete objs;
return new FaceObjects();
}
...
delete std_img;
delete outputs;
}
+ if (objs2) delete objs2; else delete objs;
return faces;
(objs is uninitialized when objs2 is used, so the else matters.)
Workaround we use
Until a fixed release is available, we no longer call recognize(). We build the same pipeline from calls that do not leak: nn.YOLOv8.detect() → image.Image.affine() to the feature model input with the same 5 reference points → nn.NN.forward_image() → compare with FaceRecognizer.features using 0.5 + 0.5 * cos and a float32 threshold comparison. On 2,784 photos (5,427 calls including resized copies, 575 faces) it gives exactly the same boxes, landmarks, class ids, scores and features as recognize(), and memory stays flat over 2,000 calls. FaceRecognizer is kept only for load_faces / save_faces / add_face / remove_face.
We can open a pull request to MaixCDK with the fix above if that helps.
- Dominant language
- Python
- Stars
- 872
- Forks
- 129
- Avg merge
- 2h 5m
- Merged PRs (30d)
- 1
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from sipeed/MaixPy
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
-
Difficulty 4/5 3-5 days Newbie friendliness 45/100
Similar issues
-
Remove the `pull_request_target` workflow before GitHub blocks itPossibly taken @jat255 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
IBM/mcp-context-forge#7483 ·
Maintainers usually reply within 2 days
-
bug good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
[Bug Report]: OpenAI 辅助 API 连通性测试因输出上限仅 1 token 误报“未知错误”Possibly taken @fjfanjian claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Project-N-E-K-O/N.E.K.O#3640 ·
Maintainers usually reply within 1 day
-
author-page correction
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
acl-org/acl-anthology#10808 · 1 comment ·
Maintainers usually reply within 1 day