الإحصاء والخرائط الحرارية وتقدير السرعة
أنشئ عدّادات للخطوط، وإحصاءات لإشغال المناطق، وخرائط حرارية، وتقديرات للسرعة بالاعتماد على التتبّع، وعايرها بواقعية.
معظم أنظمة الرؤية في بيئات الإنتاج ليست model.predict()، بل تتكوّن من متتبّع وبعض العمليات الهندسية: خط تعبره الأجسام، أو منطقة تشغلها، أو مقياس يحوّل البكسلات إلى أمتار. وتكمن أسباب الخطأ في النتائج في العمليات الهندسية: إذ يقدّر مقياس السرعة في مقطع العرض التوضيحي لهذا الدرس سرعة المشاة على أنها 1 كم/ساعة، أي ربع سرعتهم الفعلية، بسبب رقم واحد جرى تخمينه.
عملي
التتبّع مع العمليات الهندسية#
تشغّل كل فئة حلول في ultralytics.solutions وضع التتبّع داخليًا، ثم تطبّق عملياتها الهندسية الخاصة على سجلّ المسار. ثبّت الإضافات مرة واحدة باستخدام pip install "ultralytics[solutions]".
| الفئة | العمليات الهندسية | النتائج الأساسية |
|---|---|---|
ObjectCounter | نقطتان = خط، و3 نقاط أو أكثر = مضلع | in_count, out_count, classwise_count |
RegionCounter | مضلع واحد أو أكثر بأسماء محددة | region_counts |
Heatmap | يجمع مساحات الصناديق عبر الزمن | plot_im (بالإضافة إلى الإحصاء عند تحديد منطقة) |
SpeedEstimator | إزاحة المسار × meter_per_pixel | speed_dict (كم/ساعة لكل معرّف مسار) |
تستخدم الحلول tracker="tracktrack.yaml" افتراضيًا؛ مرّر المتتبّع الذي تحققت من صلاحيته في الدرس السابق.
إحصاء عبور الخطوط#

خط الإحصاء هو قطعة مستقيمة. يُحصى الجسم مرة واحدة عند عبور مركز صندوقه الخط بين إطارين، ويحدد الجانب الذي أتى منه ما إذا كان يُضاف إلى IN أو OUT. لذلك يتطلب سير النقل أعلاه التتبّع: فمن دون المعرّف، سيُحصى الصندوق #015 مجددًا في كل إطار يلامس فيه الخط.
import cv2
from ultralytics import solutions
from ultralytics.utils.downloads import safe_download
video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video)) # 640x360, 30 FPS, 62 frames
w, h, fps = (int(cap.get(p)) for p in (cv2.CAP_PROP_FRAME_WIDTH, cv2.CAP_PROP_FRAME_HEIGHT, cv2.CAP_PROP_FPS))
writer = cv2.VideoWriter("counted.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
counter = solutions.ObjectCounter(
model="yolo26n.pt",
region=[(0, 180), (640, 180)], # two points: a horizontal line across the middle of the frame
classes=[0], # person
tracker="bytetrack.yaml",
verbose=False,
)
while True:
ok, frame = cap.read()
if not ok:
break
results = counter(frame) # detect + track + count in one call
writer.write(results.plot_im)
cap.release()
writer.release()
print(f"in={results.in_count} out={results.out_count} classwise={results.classwise_count}")in=2 out=2 classwise={'person': {'IN': 2, 'OUT': 2}}عبر أربعة أشخاص منتصف الساحة خلال ثانيتين في المقطع: اثنان في كل اتجاه. باستخدام إعداد TrackTrack الافتراضي، يعرض السكربت نفسه in=0 out=1 لأن انخفاض عدد المسارات يعني انخفاض عدد مرات العبور. افتح counted.mp4 وتحقق بصريًا من كل عبور قبل الوثوق بأي رقم.

على الطريق، يقسم الخط نفسه حركة المرور حسب الاتجاه: تُحتسب المركبات التي تعبر إلى الأعلى ضمن IN، والتي تعبر إلى الأسفل ضمن OUT. ضع الخط في موضع تعبره فيه الأجسام بوضوح وبزاوية قائمة، بعيدًا عن حواف الإطار والعوائق والطرف البعيد من الطريق حيث تكون الصناديق صغيرة.
إحصاء الإشغال في منطقة#
يجيب المضلع الذي يحدد المنطقة عن السؤال: «كم عدد الأجسام داخل المنطقة الآن؟» يتضمن دليل عدّ المناطق عنصرًا جاهزًا هو RegionCounter؛ وتتم العملية في أساسها باختبار واحد للنقطة داخل المضلع لكل صندوق متتبّع:
from shapely.geometry import Point, Polygon
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
zone = Polygon([(100, 100), (500, 100), (500, 300), (100, 300)]) # pixel coordinates
peak = 0
for r in model.track(video, stream=True, tracker="bytetrack.yaml", classes=[0], verbose=False):
if not r.boxes.is_track:
continue
inside = sum(zone.contains(Point(cx, cy)) for cx, cy in r.boxes.xywh[:, :2].tolist())
peak = max(peak, inside)
print(f"peak occupancy: {peak} people inside the zone")peak occupancy: 10 people inside the zonexywh[:, :2] هو مركز كل صندوق. ولإحصاء الطوابير ومدة المكوث، احتفظ بطابع زمني لكل معرّف يحدد وقت دخوله المنطقة.
تقدير السرعة#

تأتي كل تسمية أعلاه من المسار خلف المركبة: المسافة التي قطعها مسارها بالبكسل خلال عدد معلوم من الإطارات. السرعة = المسافة بالبكسل × الأمتار لكل بكسل ÷ الزمن المنقضي. ويحدد مدخلان مدى صحة النتيجة:
- الأمتار لكل بكسل. قِس طولًا معلومًا في المشهد (بلاطة أرضية، أو علامة مسار، أو موقف سيارات) عند العمق الذي تتحرك فيه الأجسام. أما في حالة الكاميرات المائلة، فاستخدم تحويلًا متجانسًا من أربع نقاط لرسم مستوى الأرض بدلًا من ذلك.
- معدل الالتقاط الفعلي. تتناسب السرعة طرديًا مع FPS؛ إذ تُضرب جميع قيم السرعة في العامل نفسه عند إعادة ترميز اللقطات أو إبطائها.
تقيس هذه الحلقة من منتصف أسفل كل صندوق (حيث يلامس الجسم الأرض) وتحسب متوسط كل مسار على مدى 10 إطارات:
from collections import defaultdict, deque
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
fps = 30 # the real capture rate, not the playback rate
pixels_per_meter = 33 # example: a 1.2 m floor feature spans 40 px; measure your own scene
prev = {} # track ID -> last bottom-center point
window = defaultdict(lambda: deque(maxlen=10)) # about 0.3 s of speeds per track at 30 FPS
for r in model.track(video, stream=True, tracker="bytetrack.yaml", classes=[0], verbose=False):
if not r.boxes.is_track:
continue
for (x1, y1, x2, y2), tid in zip(r.boxes.xyxy.tolist(), r.boxes.id.int().tolist()):
cx, cy = (x1 + x2) / 2, y2 # bottom-center sits on the ground plane
if tid in prev:
px, py = prev[tid]
meters = ((cx - px) ** 2 + (cy - py) ** 2) ** 0.5 / pixels_per_meter
window[tid].append(meters * fps * 3.6) # km/h
prev[tid] = (cx, cy)
smoothed = sorted(sum(s) / len(s) for s in window.values() if len(s) == s.maxlen)
print(f"{len(smoothed)} tracks; median {smoothed[len(smoothed) // 2]:.1f} km/h")29 tracks; median 2.1 km/hيمشي الناس بسرعة 4–5 km/h، لذا فإن 2.1 km/h تعني أن مقياس المثال غير صحيح لهذا المشهد: لم تُقَس قيمة pixels_per_meter هنا. وهذا التحقق المعقول، أي مقارنة الوسيط بسرعة تعرفها، هو العادة المهمة.
تُطبّق solutions.SpeedEstimator الفكرة نفسها في بيئة الإنتاج: فهي تقيس مركز صندوق كل مسار على مدى max_hist إطارًا، وتحوله باستخدام meter_per_pixel، وتثبّت سرعة واحدة لكل معرّف (بحد أقصى max_speed):
import cv2
from ultralytics import solutions
from ultralytics.utils.downloads import safe_download
video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))
speed = solutions.SpeedEstimator(
model="yolo26n.pt",
classes=[0],
tracker="bytetrack.yaml",
fps=30, # the real capture rate, not the playback rate
meter_per_pixel=0.03, # a guess: a ~0.45 m shoulder width spans ~15 px here
max_hist=15, # 0.5 s of history before a speed is locked
verbose=False,
)
speeds = {}
while True:
ok, frame = cap.read()
if not ok:
break
speeds.update(speed(frame).speed_dict) # {track_id: km/h} once locked
cap.release()
kmh = sorted(speeds.values())
print(f"{len(kmh)} tracks; median {kmh[len(kmh) // 2]} km/h, range {kmh[0]}-{kmh[-1]} km/h")25 tracks; median 1 km/h, range 0-4 km/hما زالت القيمة أقل بنحو 4×، للسبب نفسه: مقياس مُخمّن بناءً على حجم الجسم. مركز الصندوق مناسب للكاميرات العلوية مثل هذه؛ أما مع الكاميرات المائلة، ففضّل حلقة منتصف أسفل الصندوق أو التحويل الإسقاطي.
تجميع خريطة حرارية#

تضيف الخريطة الحرارية حرارة أسفل كل صندوق متتبَّع في كل إطار، فتتوهج المسارات أو الممرات التي تشهد أكبر حركة مرور بأقصى سطوع. امنحها region وستعدّ الكائنات أيضًا، كما في الطريق السريع أعلاه:
import cv2
from ultralytics import solutions
from ultralytics.utils.downloads import safe_download
video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video))
heatmap = solutions.Heatmap(
model="yolo26n.pt",
classes=[0],
tracker="bytetrack.yaml",
colormap=cv2.COLORMAP_PARULA,
verbose=False,
)
while True:
ok, frame = cap.read()
if not ok:
break
results = heatmap(frame)
cap.release()
cv2.imwrite("heatmap.jpg", results.plot_im) # الإطار الأخير مع تراكب الحرارة المتراكمةعلى مدى ساعات من اللقطات، يُظهر التراكب نفسه الممرات المزدحمة والمناطق غير المستخدمة والمسارات التي يسلكها الأشخاص فعلًا. ويُبنى كل من مراقبة التمارين وواجهة الاستدلال المباشر باستخدام Streamlit من اللبنات نفسها.
مواطن الخلل#
العَرَض: تعمل معالجة عدّ الإطارات بنصف FPS المتوقع. الحل: تنفّذ ObjectCounter وHeatmap وSpeedEstimator الكشف والتعقب داخليًا. لا تضعها ضمن حلقة model.track() خاصة بك ولا تمرّر إليها r.orig_img؛ بل زوّدها بإطارات خام.
العَرَض: تبلغ سرعة المشاة 1 km/h والسيارات 300 km/h. الحل: قِس meter_per_pixel انطلاقًا من طول معروف تتحرك الأجسام على امتداده، وتأكد من FPS الفعلي للكاميرا، وقارن الوسيط بسرعة معروفة (سرعة المشي أو الحد المعلن للسرعة).
يرتفع المركز الهندسي كلما اقترب جسم من الكاميرا وكبر صندوقه. ويقدّر المركز السفلي موضع ملامسة الجسم للأرض، لذا تتبع المسافات المقاسة منه الحركة الفعلية بدقة أكبر بكثير.
عرض الحل
عاير باستخدام شيء يمكنك قياسه في الإطار، ثم تحقّق من معقولية النتيجة:
# Two points along a floor feature whose real length you know (read them off a saved frame)
(x1, y1), (x2, y2) = (100, 300), (140, 300)
real_length_m = 1.2 # e.g. measured tile width or lane marking
pixels = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print(f"pixels_per_meter = {pixels / real_length_m:.1f}, meter_per_pixel = {real_length_m / pixels:.4f}")pixels_per_meter = 33.3, meter_per_pixel = 0.0300ضع pixels_per_meter في speed_loop.py (أو meter_per_pixel في SpeedEstimator) ثم أعد التشغيل. إذا ظلت سرعة المشاة بعيدة عن 4–5 km/h، فقد قيس المقياس عند عمق غير مناسب أو أن قيمة FPS غير صحيحة: فإبطاء المقطع إلى نصف سرعته يخفض كل قراءة إلى النصف. في المقطع التجريبي، استخدم كلا السكربتين المقياس المُخمّن نفسه (0.03 m لكل بكسل تعادل 33 بكسلًا لكل متر)، وكانت القراءات أقل بمقدار 2–4×؛ لذا إما أن البكسل هنا يغطي مسافة أكبر مما افترضه التخمين، أو أن معدل الالتقاط الفعلي للمقطع ليس 30 FPS. ولا يحسم الأمر إلا القياس داخل المشهد.