Объём груза нужен везде, где считаются деньги: приёмка на складе, расчёт стоимости доставки, контроль недогруза фуры, учёт сырья в бункере. Ручной обмер рулеткой медленный, спорный и не оставляет следов — а камера глубины за одну секунду даёт автоматический замер с сохранённой «фотографией» объёма, к которой можно вернуться при споре. В этом гайде соберём измеритель на Python: камера Orbbec висит над зоной, скрипт строит карту высот над плоскостью основания и интегрирует объём через интринсики камеры. Никакой магии: школьная стереометрия, аккуратно применённая к каждому пикселю. В качестве камеры возьмём Astra 2 со структурированным светом — в помещении на дистанциях 0,6–2 м она даёт плотную глубину, которой достаточно для замера с приемлемой ошибкой; вся логика переносится на любую другую Orbbec без изменений.
Тот же каркас решает родственный круг задач: контроль остатка насыпного груза в корзине, проверка «сколько места осталось» в контейнере перед закрытием, подсчёт свободного объёма полки. Везде формула одна — карта высот над известной плоскостью, — различаются лишь плоскость, пороги и правила браковки замера. Это стоит понимать как плюс: один и тот же код после калибровки обслуживает несколько постов, и обслуживание не размножается.
Что понадобится
- камера Orbbec (пример на Astra 2), жёсткое крепление над зоной замера;
- Python 3.8+ с pyorbbecsdk, NumPy, OpenCV;
- ровная плоскость основания (стол, паллета на ровном полу);
- рулетка для одноразовой проверки точности на тестовом коробе;
- известные интринсики камеры (fx, fy, cx, cy) — из средств SDK.
Пошаговая настройка
Шаг 1. Идея и геометрия замера
Камера смотрит сверху вниз. Каждый пиксель карты глубины — расстояние вдоль луча до поверхности. Если мы знаем плоскость основания (стол или пол), то высота груза в точке (u, v) — это зазор между плоскостью и измеренной поверхностью вдоль луча. Площадь, которую покрывает один пиксель на дистанции z, равна z*z/(fx*fy) — это прямое следствие модели камеры-обскуры. Объём — сумма «площадь пикселя × высота» по всем пикселям груза. Всё; дальше только аккуратность: плоскость меряем не «на глаз», а фитированием по пустой зоне, высоты режем порогом шума, дырки данных исключаем из суммы.
Монтаж определит точность сильнее любого кода. Вешайте камеру так, чтобы самый высокий штатный груз попадал в зону резкого фокуса и не подходил к камере ближе нижней границы рабочего диапазона; угол наклона — минимально необходимый: чем ближе взгляд к вертикали, тем честнее «высота вдоль луча» совпадает с привычной вертикальной высотой. Проверьте на месте и засветку: прожекторы под потолком, окна и отражающие вывески дают блики, которые потом выглядят как «загадочные дырки» в данных.
Шаг 2. Калибруем плоскость основания
При монтаже зафиксируйте камеру и один раз снимите пустую зону. Плоскость фитируем по валидным точкам методом наименьших квадратов в координатах лучей — практично работать в системе камеры: точка луча на дистанции z это ((u-cx)*z/fx, (v-cy)*z/fy, z). Плоскость в этой системе: a*x + b*y + c*z = 1. Фит по сотням тысяч точек пустого пола даёт устойчивые коэффициенты даже при небольшом наклоне монтажа:
import numpy as np
from pyorbbecsdk import Pipeline, Config, OBStreamType
FX, FY, CX, CY = 455.0, 455.0, 320.0, 200.0 # интринсики вашей камеры
def grab_depth_m(pipeline) -> np.ndarray:
frames = pipeline.wait_for_frames(100)
if frames is None:
raise TimeoutError("no frameset")
d = frames.get_depth_frame()
h, w = d.get_height(), d.get_width()
depth = np.asarray(d.get_data(), dtype=np.uint16).reshape(h, w)
return depth.astype(np.float32) / 1000.0 # метры
pipeline = Pipeline()
config = Config()
config.enable_stream(OBStreamType.OB_STREAM_DEPTH)
pipeline.start(config)
empty = grab_depth_m(pipeline) # пустая зона при монтаже
h, w = empty.shape
u = np.arange(w)[None, :].repeat(h, 0).astype(np.float32)
v = np.arange(h)[:, None].repeat(w, 1).astype(np.float32)
m = empty > 0.2
x = (u[m] - CX) * empty[m] / FX
y = (v[m] - CY) * empty[m] / FY
z = empty[m]
A = np.stack([x, y, z], axis=1)
coef, *_ = np.linalg.lstsq(A, np.ones_like(z), rcond=None)
a, b, c = coef # плоскость: a*x + b*y + c*z = 1
Шаг 3. Считаем объём
Рабочий скрипт: снимаем кадр с грузом, считаем для каждого пикселя высоту вдоль луча (где плоскость пересекает этот луч), отсекаем шум, суммируем объём только по пикселям груза:
def measure_volume(depth: np.ndarray,
coef, z_ref=1.2, h_min=0.01) -> float:
# объём груза в кубометрах; z_ref - номинальная дистанция до плоскости (м),
# h_min - порог высоты, отсекающий шум (10 мм)
a, b, c = coef
h, w = depth.shape
u = np.arange(w)[None, :].repeat(h, 0).astype(np.float32)
v = np.arange(h)[:, None].repeat(w, 1).astype(np.float32)
# дистанция до плоскости вдоль луча пикселя (u, v)
denom = a * (u - CX) / FX + b * (v - CY) / FY + c
z_plane = 1.0 / denom
valid = depth > 0.2
height = np.where(valid, z_plane - depth, 0.0) # высота над плоскостью
box = height > h_min # только груз
zpix = depth[box]
pixel_area = (zpix * zpix) / (FX * FY) # м^2 на пиксель
volume = float(np.sum(pixel_area * height[box])) # м^3
return volume
box_depth = grab_depth_m(pipeline)
print(f"Объём: {measure_volume(box_depth, coef):.4f} м3")
Шаг 4. Проверяем на эталоне
Возьмите короб известного объёма (производитель картона указывает габариты), поставьте в зону, сравните. Проверьте три высоты укладки и сдвиг короба по зоне — цифры должны воспроизводиться в пределах единиц процентов. Затем проверьте «худший» груз: тёмный матовый меш, бликующую плёнку, открытую паллету с неровным верхом. Так вы узнаете реальную погрешность вашей сцены, а не паспортную — и заложите её в бизнес-логику (например, округление и допуски при приёмке).
Шаг 5. Контроль качества каждого замера
Автоматический обмер без самоконтроля рано или поздно выдаст мусор с уверенностью слона. Добавьте к каждому замеру три числа: долю валидных пикселей в ROI, долю пикселей, отсечённых порогом высоты, и разброс по нескольким последовательным кадрам. Правила браковки формулируются из этих чисел: доля валидных ниже порога — «не измерено, нужен ручной обмер»; кадры расходятся сильнее допуска — «зона не пустая, повторить». Каждый замер сохраняйте как протокол: JSON с коэффициентами плоскости, числами контроля и ссылкой на исходный depth-кадр — при споре с контрагентом у вас останется доказательство, а не только цифра.
Разбор кода
Почему фит плоскости, а не «вычесть константу»? Монтаж всегда чуть наклонён, а структурированный свет и стерео дают систематический наклон, если камера стоит под углом. Вычитание одной константы превращает этот наклон в клин: половина короба «занижена», половина «завышена», и ошибка растёт с площадью груза. Фит по пустой зоне съедает наклон целиком, и пересчитывается за секунду при каждом переносе крепления — храните коэффициенты рядом с настройками точки замера.
Почему считаем всё в системе координат камеры, а не в «мировой» системе склада? Для объёма это не нужно: объём инвариантен к повороту, а фит плоскости в лучевых координатах устойчив и не требует внешней калибровки положения камеры. Внешняя калибровка понадобилась бы только при слиянии нескольких камер над одной зоной — тогда сначала сводите облака в общий базис (мы разбирали эту задачу отдельно), а дальше всё тот же конвейер карты высот. Для одиночной камеры держите систему как можно проще: меньше преобразований — меньше мест для тихой ошибки.
Формула площади пикселя z²/(fx·fy) — сердце метода. Обратите внимание: площадь зависит от дистанции, поэтому суммировать надо именно «площадь × высота» по каждому пикселю, а не умножать общую площадь зоны на среднюю высоту. Порог h_min (у нас 10 мм) отсекает шум глубины: без него объём «раздувается» тонким слоем по всей пустой зоне. Порог по валидности (depth больше 0.2) убирает нули-дырки, которые иначе прошли бы как «высота равна дистанции до плоскости» — классическая ошибка, дающая завышение в разы.
Отдельно про ROI: если зона замера фиксирована, ограничьте сумму пикселей прямоугольником зоны — это исключит прохожих и края стеллажей. Для автоматической обрезки под короб подойдёт морфология OpenCV по маске box: largest connected component + bounding rect.
И про шум: глубина по определению шумит, и одиночный кадр — это одна реализация шума. Снятие трёх-пяти кадров с интервалом в 100 мс и усреднение высоты (не глубины!) по валидным пикселям стабилизирует показание заметно сильнее, чем кажется, — особенно на мягкой таре, где поверхность чуть «дышит». Бонус этого же приёма — бесплатная проверка: если пять кадров дали разброс больше допуска, замер бракуется автоматически по правилам из шага контроля качества.
| Фактор | Влияние на ошибку | Что делать |
|---|---|---|
| Наклон крепления | клиновая ошибка по площади | фит плоскости, перефиж при переносе |
| Дырки данных (плёнка, тёмное) | занижение объёма | контроль доли невалидных пикселей, браковать кадр |
| Шум глубины | раздувание тонким слоем | порог высоты 10–15 мм |
| Блики и солнце | выбросы, срыв измерения | зашторить зону, избегать прямого солнца |
Суммарная ошибка складывается из худших факторов сцены, а не средних: один блестящий угол может испортить замер, который на 99% площади идеален. Поэтому контрольные числа шага 5 важнее любого уточнения формул — они превращают «иногда врёт» в «иногда честно отказывается мерить».
Частые проблемы
Объём завышен в разы
Почти всегда это дырки данных, просуммированные как «высота»: невалидный нулевой пиксель даёт высоту, равную дистанции до плоскости. Проверьте, что в сумму идут только пиксели с валидной глубиной и высотой выше порога. Второй кандидат — перепутаны единицы: глубина в миллиметрах против плоскости в метрах; у нас всё приводится к метрам сразу, и эта привычка избавляет от целого класса ошибок. Для контроля выведите карту высот в окно OpenCV с цветовой схемой JET — глазами сразу видно, где груз, а где мусор.
Тёмные и блестящие грузы не меряются
Ограничение технологии, а не кода: структурированный свет слабо читается на чёрном матовом и зеркальном. Практика: введите в протокол замера долю валидных пикселей в ROI; если она ниже порога — кадр помечается «требует ручного обмера», вместо того чтобы тихо выдать заниженный объём. На таких постах с большим диапазоном дистанций или сложной тарой выигрышнее iToF-камера уровня Femto Bolt — но и у неё зеркала остаются запретной зоной.
Груз вылезает за зону обзора
Крупная паллета частично вне кадра — это занижение объёма без единого предупреждения в коде. Добавьте проверку полноты: если валидные пиксели груза доходят до границы ROI, замер помечается «требует переустановки груза». Полное решение — расширить кадр или перенести камеру выше; частичное — две камеры с перекрытием зон и слияние облаков. Что точно не работает — молча интегрировать то, что видно: на приёмке такие цифры превращаются в регулярные недостачи, которые ищут месяцами.
Замер «плывёт» день ото дня
Крепление ослабло, стол сдвинули, температура изменила геометрию. Раз в смену снимайте контрольный эталон известного объёма; если цифра уехала за допуск — перефитуйте плоскость по пустой зоне (это одна команда в скрипте). Жёсткое крепление на кронштейне, а не на стеллаже, само по себе убирает большинство таких дрейфов.
Подобрать камеру под автоматический обмер можно в каталоге Orbbec на luwam.ru. Для настольных и стеллажных зон обычно берут Astra 2 (структурированный свет, плотная глубина в помещении), для больших зон и улицы — Femto Bolt. Поможем с расчётом геометрии поста и консультацией по SDK, выставим счёт для юрлица, гарантия 1 год.
Камеры Orbbec в наличии на LUWAM
- Orbbec Femto Bolt — iToF 4K, замена Azure Kinect — 99 900 ₽
- Orbbec Gemini 335L — стерео для роботов и SLAM — 99 900 ₽
- Orbbec Astra 2 — структурированный свет, сканирование помещений — 99 900 ₽
Весь каталог Orbbec · консультация по SDK · счёт для юрлица в день обращения · гарантия 1 год.

