Инженерный тур. 2 этап
Участникам заключительного этапа предстоит разработать систему автоматизированного мониторинга объектов топливно-энергетического комплекса при помощи квадрокоптера.
Чтобы успешно решить данную задачу необходимо:
- знать и понимать принципы устройства и работы квадрокоптера, принципы базовой и программной настройки квадрокоптера;
- обладать навыками 3D-моделирования, программирования, работы с компьютерным зрением и машинным обучением.
Задания второго отборочного этапа готовят команды к решению задачи заключительного этапа и представляют собой его декомпозированные компетентностные подзадачи. Например: программирование блока управления на Python, программная настройка квадрокоптера, автономный полет квадрокоптера и обработка данных, машинное обучение.
Пакет заданий состоит из индивидуальных задач, проверяющих навыки участников команды согласно их компетенциям, а также для того, чтобы каждый участник мог попробовать себя в другой роли.
Командное задание позволяет участникам апробировать свои решения в симуляторе: инженерам — настроить квадрокоптер и подготовить виртуальный мир, а программистам — написать программный код для автономного полета квадрокоптера. Для успешного выполнения заданий второго отборочного этапа участникам необходимо правильно распределить задачи, а также наладить систему планирования и коммуникации внутри команды.
Компетенции, необходимые участникам для решения задач второго этапа:
- базовые навыки работы с летающими робототехническими системами;
- программирование (Python);
- навыки работы с компьютерным зрением (OpenCV);
- 3D-моделирование;
- Front-end разработка.
Индивидуальные задачи второго этапа инженерного тура открыты для решения. Соревнование доступно на платформе Яндекс.Контест: https://contest.yandex.ru/contest/69899/enter/.
Ознакомьтесь с видео и отметьте все электронные компоненты и части коптера, которые были подключены или установлены неправильно (ссылка на видео: https://disk.yandex.ru/i/0AeXVnc6tzUWHQ).
- Подключение шлейфа радиоприемника к полетному контроллеру.
- Подключение шлейфа радиоприемника к радиоприемнику.
- Подключение питания к полетному контроллеру.
- Подключение сигнального провода регуляторов оборота от мотора № 1 к полетному контроллеру.
- Подключение сигнального провода регуляторов оборота от мотора № 2 к полетному контроллеру.
- Подключение сигнального провода регуляторов оборота от мотора № 3 к полетному контроллеру.
- Подключение сигнального провода регуляторов оборота от мотора № 4 к полетному контроллеру.
- Подключение силовых проводов от регулятора оборотов мотора № 1 к плате распределения питания.
- Подключение силовых проводов от регулятора оборотов мотора № 2 к плате распределения питания.
- Подключение силовых проводов от регулятора оборотов мотора № 3 к плате распределения питания.
- Подключение силовых проводов от регулятора оборотов мотора № 4 к плате распределения питания.
- Подключение питания светодиодной ленты.
- Подключение сигнального провода светодиодной ленты к Raspberry Pi.
- Подключение питания (5V) к Raspberry Pi.
- Подключение лазерного дальномера к Raspberry Pi.
- Подключение шлейфа от камеры к Raspberry Pi.
- Подключение проводной связи от полетного контроллера к Raspberry Pi.
- Установка пропеллера на мотор № 1.
- Установка пропеллера на мотор № 2.
- Установка пропеллера на мотор № 3.
- Установка пропеллера на мотор № 4.
Для решения задачи необходимо внимательно посмотреть видео по сборке квадрокоптера из образовательного курса: https://stepik.org/lesson/1431188/step/7?unit=1449603 или ознакомиться с документацией по сборке квадрокоптера: https://clover.coex.tech/ru/assemble_4_2_ws.
1, 3, 4, 5, 14, 15, 16, 17, 18, 20, 21.
Ознакомьтесь с видео и отметьте все ошибки, допущенные при настройке квадрокоптера (ссылка на видео: https://disk.yandex.ru/i/CqE4B18KRF-JMw).
- При загрузке прошивки в полетный контроллер.
- В выборе конфигурации рамы квадрокоптера.
- При калибровке компаса.
- При калибровке гироскопа.
- При калибровке акселерометра.
- При калибровке уровня горизонта.
- При установке ориентации полетного контроллера.
- При калибровке радиоаппаратуры управления.
- При настройке режимов полетного_ контроллера.
- При назначении аварийного отключения моторов.
- При настройке параметров питания.
Для решения задачи необходимо внимательно посмотреть видео по сборке квадрокоптера из образовательного курса: https://stepik.org/lesson/1431188/step/7?unit=1449603 или ознакомиться с документацией по сборке квадрокоптера: https://clover.coex.tech/ru/assemble_4_2_ws.
7, 8, 10, 11.
Ознакомьтесь с видео и отметьте все ошибки, допущенные при настройке образа квадрокоптера (ссылка на видео: https://disk.yandex.ru/i/Qm5kl5WyMX6jhg).
- Настройка параметра
aruco. - Настройка параметра
aruco_detect. - Настройка параметра
aruco_map. - Настройка параметра
aruco_vpe. - Настройка параметра
map. - Настройка размера
aruco-маркера по умолчанию. - Настройка параметра
direction_z. - Настройка параметра
direction_y. - Настройка параметров карты: длина маркера.
- Настройка параметров карты: количество маркеров по оси \(X\).
- Настройка параметров карты: количество маркеров по оси \(Y\).
- Настройка параметров карты: расстояние между центрами меток по оси \(X\).
- Настройка параметров карты: расстояние между центрами меток по оси \(Y\).
- Настройка параметров карты: номер первого маркера.
- Настройка параметров карты: «ключ» начала нумерации при генерации карты.
- Перезагрузка пакетов клевера на образе.
Примечания
Сборка квадрокоптера является стандартной.Для решения задачи необходимо внимательно посмотреть видео по сборке квадрокоптера из образовательного курса: https://stepik.org/lesson/1431188/step/7?unit=1449603 или ознакомиться с документацией по сборке квадрокоптера: https://clover.coex.tech/ru/assemble_4_2_ws.
5, 7, 8, 9, 15.
Ознакомьтесь с видео и по характеру полета квадрокоптера соотнесите их с подобранными коэффициентами PID-регулятора.
Ссылки на видео:
- https://disk.yandex.ru/d/uj6JXzlr7uSDFQ/01пид.mp4.
- https://disk.yandex.ru/d/uj6JXzlr7uSDFQ/02пид.mp4.
- https://disk.yandex.ru/d/uj6JXzlr7uSDFQ/03пид.mp4.
- https://disk.yandex.ru/d/uj6JXzlr7uSDFQ/04пид.mp4.
Примечания
Сборка квадрокоптера Clever 4 является стандартной. Для демонстрации менялся один из коэффициентов относительно рекомендованного значения в прошивке PX4 полетного контроллера.MC_PITCHRATE_P = 0.087 MC_PITCHRATE_I = 0.037 MC_PITCHRATE_D = 0.0044 MC_PITCH_P = 8.5 MC_ROLLRATE_P = 0.015 MC_ROLLRATE_I = 0.037 MC_ROLLRATE_D = 0.0044MC_PITCHRATE_P = 0.087 MC_PITCHRATE_I = 0.037 MC_PITCHRATE_D = 0.0044 MC_PITCH_P = 8.5 MC_ROLLRATE_P = 0.087 MC_ROLLRATE_I = 0.037 MC_ROLLRATE_D = 0.010MC_PITCHRATE_P = 0.300 MC_PITCHRATE_I = 0.037 MC_PITCHRATE_D = 0.0044 MC_PITCH_P = 8.5 MC_ROLLRATE_P = 0.087 MC_ROLLRATE_I = 0.037 MC_ROLLRATE_D = 0.0044MC_PITCHRATE_P = 0.020 MC_PITCHRATE_I = 0.037 MC_PITCHRATE_D = 0.0044 MC_PITCH_P = 8.5 MC_ROLLRATE_P = 0.087 MC_ROLLRATE_I = 0.037 MC_ROLLRATE_D = 0.0044
Для решения задачи необходимо внимательно посмотреть видео по сборке квадрокоптера из образовательного курса: https://stepik.org/lesson/1431188/step/7?unit=1449603 или ознакомиться с документацией по настройке квадрокоптера Clover: https://clover.coex.tech/ru/setup.html#усредненные-коэффициенты-pid-для-клевера-4.
1 — D, 2 — C, 3 — A, 4 — B.
Вы удаленно работаете инженером-программистом БПЛА в компании, которая занимается мониторингом объектов топливно-энергетического комплекса (ТЭК).
Для выполнения мониторинга был создан квадрокоптер, работающий на платформе ROS, с использованием образа Clover: https://clover.coex.tech/ru/image.html.
Коллеги, находящиеся за километры от вас на объекте ТЭК, столкнулись с проблемой. Во время последней миссии квадрокоптер патрулировал участок трубопровода, чтобы обнаружить потенциальные утечки и повреждения. В процессе выполнения задачи дрон несколько раз отклонялся от намеченной траектории.
Для того чтобы провести отладку и исправить баг, существуют два варианта: паковать сумки и ехать к коллегам, или просить их паковать rosbag файл, который позволяет сохранять данные, публикуемые в топики ROS. Конечно, вы, как уважающий себя удаленщик, выбрали второй метод.
На этом этапе НТО выполните более простую задачу — используя rosbag файл, восстановите миссию, по которой должен был пролететь дрон.
rosbag файл: https://disk.yandex.ru/d/sVxcVxwBsyXf-w.
Файл начал записываться после взлета квадрокоптера и закончил после его посадки и дизарма. Восстановите navigate и land команды, выполненные в полете (не учитывая взлет).
Каждая команда navigate была выполнена с frame_id="aruco_map". Поэтому координаты в восстановленных командах должны быть в системе координат aruco_map.
Формат входных данных
Миссия задается псевдокодом с командами
navigate(x: float, y: float, z: float)
Для простоты опустим все параметры из оригинальной команды, кроме x, y, z, но примем frame_id="aruco_map" и land().
Формат выходных данных
В ответе укажите набор команд (каждая на своей строке), которым следовал дрон.
В команде navigate указывайте имена параметров и передавайте числа, округленные до десятых (включая .0 для целых чисел).
Пример указания команды navigate: navigate(x=1.0, y=1.0, z=2.0).
Чтобы восстановить миссию дрона, используя файл rosbag, можно следовать следующему плану. Файл rosbag содержит данные, публикуемые в топики ROS, такие как координаты дрона, ориентация, данные IMU и другие телеметрические данные.
rosbag файле топиками. Для этого воспользуемся библиотекой bagpy: https://github.com/jmscslgroup/bagpy. from bagpy import bagreader
bag = bagreader("drone_flight.bag")
for t in bag.topics:
print(t)
Получим такой список:
/mavros/altitude
/mavros/battery
/mavros/estimator_status
/mavros/extended_state
/mavros/imu/data
/mavros/imu/data_raw
/mavros/imu/mag
/mavros/imu/static_pressure
/mavros/imu/temperature_imu
/mavros/local_position/odom
/mavros/local_position/pose
/mavros/local_position/velocity_body
/mavros/local_position/velocity_local
/mavros/px4flow/ground_distance
/mavros/px4flow/raw/optical_flow_rad
/mavros/px4flow/raw/send
/mavros/px4flow/temperature
/mavros/rc/out
/mavros/setpoint_position/local
/mavros/setpoint_raw/target_attitude
/mavros/setpoint_raw/target_local
/mavros/state
/mavros/time_reference
/mavros/timesync_status
/mavros/vision_pose/pose
/tf
/tf2_web_republisher/status
/tf_static
Понимаем, что были записаны не все топики, однако достаточно имеющихся топиков, связанных с setpoint (низкоуровневое задание точки полета квадрокоптера) и tf (системы координат и их преобразования).
Далее задачу можно решить двумя способами: с использованием воспроизведения rosbag-файла и с использованием геометрии, один из которых мы рассмотрим.
Этот способ более прост для понимания участниками Олимпиады, однако требует установленного ROS или виртуальной машины Clover.
Для начала запустим в терминале roscore.
После этого в другом терминале запустим rosbag play drone_flight.bag, тем самым начав передачу данных в топики, которые были записаны в BAG-файле.
Для визуализации данных в топике запустим rviz и через кнопку Add добавим отображения TF (рис. 1.1).
После добавления увидим все системы координат, которые существовали на момент записи BAG-файла (рис. 1.2).
Здесь можно увидеть, что начало системы координат navigate_target указывает на координаты точки, в которую сейчас летит дрон с использованием navigate.
После этого напишем простой код, который регистрирует изменения преобразований системы координат navigate_target относительно aruco_map. Смещение СК navigate_target относительно aruco_map будет показывать координаты точки в СК aruco_map, в которую летит дрон.
import rospy
import tf2_ros
rospy.init_node("rosbag_solution")
tfBuffer = tf2_ros.Buffer()
tf2_ros.TransformListener(tfBuffer)
rate = rospy.Rate(1)
last_coords = None
while not rospy.is_shutdown():
transform = tfBuffer.lookup_transform(
target_frame="aruco_map",
source_frame="navigate_target",
time=rospy.Time(),
timeout=rospy.Duration(10),
)
coords = (
round(transform.transform.translation.x, 2),
round(transform.transform.translation.y, 2),
round(transform.transform.translation.z, 2),
)
if coords != last_coords:
last_coords = coords
print(last_coords)
rate.sleep()
Запустим код, следом за ним запустим воспроизведение BAG-файла. Получаем следующий результат (рис. 1.3).
Первую точку откинем, так как это координата предыдущей точки полета дрона (команда navigate была выполнена до начала записи BAG-файла, что противоречит условию).
Остается подставить полученные координаты под формат ответа, не забыв про land().
navigate(x=1.0, y=1.0, z=1.0)
navigate(x=5.5, y=2.0, z=2.0)
navigate(x=9.0, y=6.0, z=1.5)
navigate(x=5.0, y=5.0, z=2.5)
land()
После исправления ошибки, возникшей на дроне, необходимо провести мониторинг участка магистрального нефтепровода.
Магистральный нефтепровод — это сложная инженерная система, включающая в себя линейную часть, головные и промежуточные перекачивающие станции, резервуарные парки и другие сооружения. Каждому типу сооружений присвоен определенный цвет.
Напишите ROS-ноду, которая выполняет следующие действия:
- взлет квадрокоптера;
- включение светодиодной ленты заданного цвета, коды цветов: https://gist.github.com/ntomaterials/60d0d05a6e1b47e1c6edb87fa328e011;
- облет по точкам, в которых находятся сооружения заданного цвета;
- посадка в конечной точке маршрута.
В качестве ответа создайте программу, генерирующую псевдокод ROS-ноды (см. формат выходных данных), который будет выполнять задание наиболее оптимально (кратчайшим путем).
В случае, если оптимальных решений несколько, выведите любое из них.
Формат входных данных
Первая строка содержит координаты старта \((x_{start}, y_{start})\) через пробел; вторая строка содержит координаты финиша \((x_{finish}, y_{finish})\) через пробел.
Далее поочередно идут \(1 \leqslant N \leqslant 1000\) строк, содержащие параметры каждой метки: \(color_i\) — наименование цвета \(i\)-й метки; \(x_i\), \(y_i\) — координаты расположения \(i\)-й метки.
\(0 \leqslant x_{start}, y_{start}, x_{finish}, y_{finish}, x_i, y_i < 100.\)
В конце ввода идет строка, содержащая \(color_{req}\) — наименование цвета маркеров, по которым необходимо выполнить полет.
Формат выходных данных
Псевдокод для квадрокоптера, содержащий только функции:
navigate(x: int, y: int, auto_arm: bool = False)set_effect(r: int, g: int, b: int)land()
Тестовые данные
| Номер теста | Стандартный ввод | Стандартный вывод |
|---|---|---|
1 |
71 36 8 18 Red 28 46 Red 13 39 Red 12 45 SlateBlue 12 13 SlateBlue 75 41 SlateBlue 0 1 Red 78 89 SlateBlue |
navigate(x=71, y=36, auto_arm=true) set_effect(r=255, g=0, b=0) navigate(x=75, y=41) navigate(x=12, y=13) navigate(x=0, y=1) navigate(x=8, y=18) land() |
- Зажигаем светодиодную ленту соответствующим цветом.
- Зная координаты необходимых меток, составляем все вариации полета со старта и до финиша через функцию
permutationsиз библиотекиitertools, при этом пролетая все метки. - Рассчитываем дистанцию полета от одной метки к другой: \[distance = \sqrt{(x_b-x_a)^2+(y_b-y_a)^2}.\]
- Также не забываем о том, что до первого маркера необходимо долететь со старта, а с последнего — на финиш. Получаем расстояния каждой возможной вариации и берем наименьшую из всех.
Ниже представлено решение на языке Python.
from collections import defaultdict
from itertools import permutations
COLORS = {...} # коды цветов из условия
def dist(p1, p2):
return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5
def hex_to_rgb(hex: str) -> tuple[int, int, int]:
return tuple(int(hex.replace("#", "")[i : i + 2], 16) for i in (0, 2, 4))
start = tuple(map(int, input().split()))
finish = tuple(map(int, input().split()))
colors = defaultdict(list)
while len(m := input().split()) == 3:
color, *coords = m
colors[color].append(list(map(int, coords)))
points = colors[m[0]]
led_command = "set_effect(r={}, g={}, b={})".format(*hex_to_rgb(COLORS[m[0]]))
min_perm = []
if points:
min_perm = min(
permutations(points),
key=lambda perm: sum(dist(perm[i], perm[i + 1]) for i in range(len(perm) - 1))
+ dist(start, perm[0])
+ dist(perm[-1], finish),
)
ans = (
[led_command, "navigate(x={}, y={}, auto_arm=true)".format(*start)]
+ ["navigate(x={}, y={})".format(*e) for e in min_perm]
+ ["navigate(x={}, y={})".format(*finish), "land()"]
)
print("\n".join(ans))
Для анализа инфраструктуры на одном из участков, оборудованном сложной сетью трубопроводов, был использован дрон, который проводил воздушную съемку и фиксировал состояние труб. Обработайте полученное изображение, чтобы определить количество пересечений трубопроводов на участке. Данная информация будет полезна для планирования обслуживания и оценки износа труб в местах перекрестков.
Изображение представляет собой черно-белое фото, где дороги выделены белыми линиями на черном фоне. Гарантируется, что в изображении присутствуют только белые и черные пиксели; помимо этого гарантируется, что пересекаться может не больше двух дорог.
Формат входных данных
По пути ./input.jpg будет находиться файл с входным изображением, которое необходимо обработать.
Формат выходных данных
Одно число — количество перекрестков белых линий.
Тестовые данные
| Номер теста | Стандартный ввод | Стандартный вывод |
|---|---|---|
1 |
3 |
|
2 |
4 |
Загрузка и подготовка изображения:
- Программа открывает файл с изображением (
input.png) и переводит его в черно-белый формат. Это значит, что каждый пиксель становится либо белым (значение 1), либо черным (значение 0). - Изображение превращается в массив чисел (с помощью библиотеки
numpy), чтобы его можно было легко обрабатывать программно.
- Программа открывает файл с изображением (
Выделение областей с пересечениями (с помощью свертки):
- Программа использует специальную маленькую матрицу, которую называют «ядром» (
kernel). Это матрица размером \(3\times 3\): \[\begin{matrix} 1 &1 &1\\ 1 &5 &1\\ 1 &1 &1 \end{matrix}\] - Идея в том, чтобы для каждого пикселя изображения посчитать сумму значений его соседей с такими коэффициентами. Если в этой области пересекаются линии (то есть вокруг пикселя много белых точек), то сумма будет высокой.
- Функция
convolve(из библиотекиscipy) «пробегается» по всему изображению и для каждого пикселя считает такую сумму. Получается новое изображение, где значение пикселя говорит о том, сколько вокруг него белых пикселей с учетом коэффициентов ядра.
- Программа использует специальную маленькую матрицу, которую называют «ядром» (
Нахождение точек, где могут быть пересечения:
- После свертки программа ищет все пиксели, у которых полученное значение больше или равно 8. Почему 8? Подобрано экспериментально.
- Таким образом находятся все потенциальные точки пересечения.
Группировка близко расположенных точек (кластеризация):
- Из-за того как работает свертка, одно реальное пересечение может быть представлено несколькими соседними пикселями с высоким значением. Но в данном случае нужно посчитать каждое пересечение только один раз.
- Для этого используется алгоритм
DBSCAN(из библиотекиscikit-learn). Он группирует (кластеризует) близко расположенные точки вместе. - Каждая группа точек (кластер) считается одним пересечением. То есть количество таких кластеров и будет искомым числом перекрестков.
Вывод результата:
- После группировки программа считает количество получившихся кластеров.
- Это число выводится как результат работы программы — количество пересечений труб (перекрестков белых линий).
Ниже представлено решение на языке Python.
from PIL import Image
import numpy as np
from scipy.ndimage import convolve
from sklearn.cluster import DBSCAN
def count_intersections(image_path):
image = Image.open(image_path).convert("1")
image_array = np.array(image)
kernel = np.array([
[1, 1, 1],
[1, 5, 1],
[1, 1, 1],
])
convolved = convolve(image_array.astype(np.int32), kernel, mode='constant', cval=0)
im = convolved * (255 / convolved.max())
im = im.astype(np.uint8)
intersection_points = np.where(convolved >= 8)
intersection_points = list(zip(*intersection_points))
if not intersection_points:
return 0
clustering = DBSCAN(eps=10, min_samples=2).fit(intersection_points)
n_clusters = len(set(clustering.labels_)) - (1 if -1 in clustering.labels_ else 0)
return n_clusters
if __name__ == "__main__":
print(count_intersections("input.png"))
После неудачной посадки квадрокоптера повредилась деталь крепления луча. Для крепления луча к корпусу квадрокоптера используется специализированный фланец. Для изготовления нового фланца необходима 3D-модель, но на производстве есть только чертеж данной детали. Спроектируйте фланец в CAD-программе, используя чертеж. Для контроля соответствия детали определите массу получившейся детали в CAD-программе.
Материал — алюминий (плотность: 2800 кг/м\(^3\)).
Чертеж фланца см. на рис. 1.4.
Необходимо спроектировать фланец по чертежу в CAD-программе, задать материал и взвесить модель при помощи анализа модели в CAD-программе.
1435.
Тест на знание конструкторской документации, 1 балл за каждый верный ответ.
Ответьте на вопросы:
Какие документы КД относится к текстовым?
- Чертеж детали.
- Перечень элементов.
- Схема.
- Спецификация.
Какое обозначение радиуса правильное?
Какая поверхность имеет наибольшую шероховатость?
Сечение A–A соответствует изображению...
Укажите, где правильно соединен вид и разрез.
Согласно ГОСТ 2.102-2013 из четырех вариантов ответа к текстовой документации относится только перечень элементов (б) и спецификация (г).
Правильный вариант ответа: б, г.
Согласно ГОСТ 2.102-2013 обозначение радиуса допускается только большой буквой R и численным обозначением, следующим сразу за буквой R.
Правильный вариант ответа: а.
Согласно ГОСТ 2.102-2013 шероховатость тем меньше чем меньше число ее обозначающее.
Правильный вариант ответа: б.
Согласно ГОСТ 2.102-2013 разрез — это изображение, полученное при мысленном рассечении предмета плоскостью.
Правильный вариант ответа: в.
Согласно ГОСТ 2.102-2013 правильно соединен вид и разрез только на второй картинке.
Правильный вариант ответа: б.
1 — б, г, 2 — а, 3 — б, 4 — в, 5 — б.
Командные задачи второго этапа инженерного тура открыты для решения. Соревнование доступно на платформе Яндекс.Контест: https://contest.yandex.ru/contest/69921/enter/.
Необходимо рассчитать на прочность луч квадрокоптера.
На рис. 2.1 показано сечение луча и нагрузки, действующие на него.
Рассчитайте:
- момент инерции сечения луча, мм\(^4\) (округлить до сотых);
- момент сопротивления сечения луча, мм\(^3\) (округлить до сотых);
- максимальные нормальные напряжения луча, МПа (округлить до тысячных);
- максимальные касательные напряжения луча, МПа (округлить до тысячных);
- максимальные главные напряжения луча, МПа (округлить до тысячных).
Так как сечение является квадратным, значения по оси \(X\) и \(Y\) равны.
Расчет момента инерции сечения.
Сечение луча состоит из квадратов, формула для расчета момента инерции квадрата \[I_z=\frac{h^4}{12},\] где \(h\) — сторона квадрата.
- Расчет момента инерции внешнего квадрата \[I_1=\frac{h^4}{12}=\frac{25^4}{12}=32552{,08}~\text{мм}^4.\]
- Расчет внутренних вырезов \[I_z=I_c+md^2,\] \[I_2=4\cdot \left(\frac{h^4}{12}+h^2\cdot 5{,}75\right)=4\cdot \left(\frac{9{,}5^4}{12}+9{,}5^2\cdot 5{,}75\right)=14650{,58} ~\text{мм}^4,\] где \(h=9{,}5\) мм — сторона вырезанного квадрата, \(m=5{,}75\) мм — расстояние между центром большого квадрата и маленьких квадратов.
Расчет всего сечения.
Вычитаем из момента инерции большого квадрата моменты инерции вырезов \[I_{x,y}=I_1-I_2=32552{,08}-14650{,58}=17901{,49}~\text{мм}^4.\] Момент инерции сечения: \[I_{x,y}=17901{,49}~\text{мм}^4.\]
Расчет момента сопротивления сечения.
Максимальный момент сопротивления сечения считается находится в точке максимального удаления от оси, то есть \[\frac{y}{2}=\frac{h}{2}=12{,}5~\text{мм}.\]
Формула для расчета сопротивления сечения квадрата: \[W_xy=\frac{I_{x,y}}{\frac{y}{2}}=\frac{17901{,49}}{12{,}5}=1432{,12}~\text{мм}^3.\]
Момент сопротивления сечения: \(W_{xy}=1432{,12}~\text{мм}^3\).
Расчет максимальных напряжений.
Формула для расчета нормальных напряжений в точке (формула Навье – Стокса): \[\sigma=\frac{M_z\cdot y}{I}.\]
Формула для расчета максимальных нормальных напряжений: \[\sigma_{max}=\frac{M_z}{W}.\]
Формула для расчета касательных напряжений (формула Журавского): \[\tau=\frac{Q\cdot S_z}{I_x\cdot B_y}.\]
Построим эпюру нагружения, рис. 2.2.
Рис. 2.2. Эпюра нагруженияРасчет максимальных нормальных напряжений.
Подставляем значения в формулу: \[\sigma_{\text{max}} = \frac{M_z}{W} = \frac{5000}{1432{,12}} = 34{,}913 ~\text{МПа}.\]
Максимальные нормальные напряжения: \[\sigma_{\text{max}} = 34{,}913 ~\text{МПа}.\]
Расчет максимальных касательных напряжений.
Для нахождения максимальных касательных напряжений необходимо посчитать касательные напряжения в четырех точках и построить эпюру.
Рис. 2.3. Касательные напряженияРасчет касательных напряжений в точке 1.
Формула для расчета касательных напряжений: \[\tau = \frac{Q \cdot S_z}{I_x \cdot B_y}.\]
Рассчитываем площадь отсеченной части сечения, которая находится выше точки 1. В данном случае это значение равно 0: \[A_{\text{отс1}} = 0.\]
Рассчитываем центр тяжести отсеченной части, в данном случае центр тяжести находится в точке 1: \[y_{c1} = \frac{h}{2} = \frac{25}{2} = 12{,}5 ~ \text{мм}.\]
Статический момент отсеченной части: \[S_x = y_{c1} \cdot A_{\text{отс1}} = 12{,}5 \cdot 0 = 0 ~ \text{мм}^3.\]
Считаем касательные напряжения: \[\tau_{\text{т.1}} = \frac{Q \cdot S_z}{I_x \cdot B_y} = \frac{100 \cdot 0}{17901{,49} \cdot 25} = 0 ~\text{МПа},\]
где \(B_y = 25 ~ \text{мм}\) — ширина сечения.
Расчет касательных напряжений в точке 2.
Формула для расчета касательных напряжений: \[\tau = \frac{Q \cdot S_z}{I_x \cdot B_y}.\]
Рассчитываем площадь отсеченной части сечения, которая находится выше точки 2: \[A_{\text{отс2}} = h \cdot t = 25 \cdot 2 = 50 ~ \text{мм}^2,\]
где \(t = 2 ~ \text{мм}\) — высота сечения.
Рассчитываем центр тяжести отсеченной части: \[y_{c2} = \frac{h}{2} - \frac{t}{2} = \frac{25}{2} - \frac{2}{2} = 11{,}5 ~ \text{мм}.\]
Статический момент отсеченной части: \[S_x = y_{c2} \cdot A_{\text{отс2}} = 11{,}5 \cdot 50 = 575 ~ \text{мм}^3.\]
Так как в точке 2 происходит изменение ширины сечения, посчитаем значение касательных напряжений до и после изменения: \[B_y^1 = 25 ~ \text{мм},\] \[B_y^2 = 6 ~ \text{мм}.\]
Считаем касательные напряжения в точке 1: \[\tau_{\text{т.2}}^1 = \frac{Q \cdot S_z}{I_x \cdot B_y^1} = \frac{100 \cdot 575}{17901{,49} \cdot 25} = 0{,}128 ~\text{МПа}.\]
Считаем касательные напряжения в точке 2: \[\tau_{\text{т.2}}^2 = \frac{Q \cdot S_z}{I_x \cdot B_y^2} = \frac{100 \cdot 575}{17901{,49} \cdot 6} = 0{,}535 ~\text{МПа}.\]
Расчет касательных напряжений в точке 3.
Формула для расчета касательных напряжений: \[\tau = \frac{Q \cdot S_z}{I_x \cdot B_y}.\]
Рассчитываем площадь отсеченной части сечения, которая находится выше точки 3: \[A_{\text{отс3}} = 3(b \cdot t) + (h \cdot t) = 3 \cdot (9{,}5 \cdot 2) + (25 \cdot 2) = 107 ~ \text{мм}^2,\]
где \(t = 2 ~ \text{мм}\) — высота сечения, \(b = 9{,}5 ~ \text{мм}\) — высота выреза.
Рассчитываем центр тяжести отсеченной части. Так как отсеченная фигура является сложной, воспользуемся формулой расчета центра тяжести сложных фигур: \[y_{c3}=\frac{\sum A_i \cdot x_i}{\sum A_i} ,\] где \(\sum A_i\) — площадь простой фигуры, \(x_i\) — расстояния от центра масс простой фигуры до начала системы координат.
\[y_{c3} = \frac{\sum A_i \cdot x_i}{\sum A_i} = \frac{(25 \cdot 2) \cdot 11{,}5 + (9{,}5 \cdot 2 \cdot 3) \cdot 5{,}75}{(25 \cdot 2) + (9{,}5 \cdot 2 \cdot 3)} = 8{,43692} ~ \text{мм}.\]
Статический момент отсеченной части: \[S_x = y_{c3} \cdot A_{\text{отс3}} = 8{,43692} \cdot 107 = 902{,}75 ~ \text{мм}^3.\]
Так как в точке 3 происходит изменение ширины сечения, посчитаем значение касательных напряжений до и после изменения: \[B_y^1 = 6 ~ \text{мм},\] \[B_y^2 = 25 ~ \text{мм}.\]
Считаем касательные напряжения в точке 1: \[\tau_{\text{т.3}}^1 = \frac{Q \cdot S_z}{I_x \cdot B_y^1} = \frac{100 \cdot 902{,}75}{17901{,49} \cdot 6} = 0{,}8404 ~\text{МПа}.\]
Считаем касательные напряжения в точке 2: \[\tau_{\text{т.3}}^2 = \frac{Q \cdot S_z}{I_x \cdot B_y^2} = \frac{100 \cdot 902{,}75}{17901{,49} \cdot 25} = 0{,}2017 ~\text{МПа}.\]
Расчет касательных напряжений в точке 4.
Формула для расчета касательных напряжений: \[\tau = \frac{Q \cdot S_z}{I_x \cdot B_y}.\]
Рассчитываем площадь отсеченной части сечения, которая находится выше точки 4: \[A_{\text{отс4}} = 3(b \cdot t) + (h \cdot t) + \left(h\cdot\frac{ t}{2}\right) = 3 \cdot (9{,}5 \cdot 2) + (25 \cdot 2) + \left(25\cdot \frac{ 2}{2}\right) = 132 ~ \text{мм}^2,\]
где \(t = 2 ~ \text{мм}\) — высота сечения, \(b = 9{,}5 ~ \text{мм}\) — высота выреза.
Рассчитываем центр тяжести отсеченной части. Так как отсеченная фигура является сложной, воспользуемся формулой расчета центра тяжести сложных фигур: \[y_{c4}=\frac{\sum A_i \cdot x_i}{\sum A_i} ,\] где \(\sum A_i\) — площадь простой фигуры, \(x_i\) — расстояния от центра масс простой фигуры до начала системы координат. \[\begin{aligned} y_{c4} = \frac{\sum A_i \cdot x_i}{\sum A_i} = \frac{(25 \cdot 2) \cdot 11,5 + (9,5 \cdot 2 \cdot 3) \cdot 5,75 + \left(25 \cdot\frac{ 2}{2}\right) \cdot 0,5}{(25 \cdot 2) + (9,5 \cdot 2 \cdot 3) + \left(25\cdot \frac{ 2}{2}\right)} =\\ {}= 6,9337 ~ \text{мм}. \end{aligned}\]
Статический момент отсеченной части: \[S_x = y_{c4} \cdot A_{\text{отс4}} = 6{,}9337 \cdot 132 = 915{,}25 ~ \text{мм}^3.\]
Считаем касательные напряжения: \[\tau_{\text{т.4}} = \frac{Q \cdot S_z}{I_x \cdot B_y^1} = \frac{100 \cdot 915{,}25}{17901{,49} \cdot 25} = 0{,}2045 ~\text{МПа}.\]
Эпюра касательных напряжений
Рис. 2.4. Эпюра касательных напряженийИз этого следует, максимальные касательные напряжения: \[\tau_{\text{max}} = 0{,}8404 ~\text{МПа}.\]
Расчет максимальных главных напряжений
Общая формула расчета главных напряжений: \[\sigma_{\text{max}} = \frac{\sigma}{2} + \frac{1}{2} \sqrt{\sigma^2 + 4\tau^4}.\]
Так как касательные напряжения много меньше нормальных напряжений и в точке 4 действуют максимальные нормальные напряжения, а касательные равны 0, то максимальные главные напряжения равны максимальным нормальным.
Максимальные главные напряжения: \[\sigma_{\text{max}} = 34{,}913 ~\text{МПа}.\]
17901,49; 1432,12; 34,913; 0,84; 34,913.
Легенда
Для мониторинга угольного завода используются квадрокоптеры на базе ROS. Крыша у каждого строения, расположенного на территории завода, имеет свой цвет и определяется его назначением:
- административные здания имеют красные крыши,
- лаборатории — зеленые,
- вход в шахту — желтый,
- здания для обогащения угля — синие.
Разработайте систему, которая позволит операторам эффективно управлять работой дронов на заводе. Представьте работу системы перед тем, как ее использование будет одобрено на реальном заводе.
Задание
- Установить образ симулятора Gazebo или ПО на свою систему.
Подготовить
bash/pythonскрипт для настройки образа симулятора (включение навигации поaruco-картам и т. д.).- Настройка
clover.launch. - Настройка
aruco.launch\verb. - Дополнительные настройки, необходимые для выполнения задания.
- Настройка
Подготовить
bash/pythonскрипт для случайной генерации мира.- Использовать эту модель https://github.com/bart02/dronepoint в качестве зданий.
- Установить здания в количестве 5 шт.
- Минимальное расстояние между центрами зданий 1 м.
- Каждое здание должно находиться в пределах \(x \in [0{,}9]\), \(y \in [0{,}9]\).
- Координаты и цвет каждого здания генерируются случайно.
Подготовить
pythonскрипт (ROS-ноду) для выполнения полета и выполнения сканирования.- При сканировании определить координаты здания в системе
aruco_mapи цвет его крыши. - На миссию ограничено количество взлетов и посадок — 1.
- Возврат на старт после выполнения сканирования.
- Все результаты сканирования записывать в топик (название
/buildings, тип данных на усмотрение команды).
- При сканировании определить координаты здания в системе
Разработать веб-приложение с графическим интерфейсом пользователя, в котором будут находиться кнопки управления миссией (старт, пауза, стоп), найденные здания с привязкой к карте.
Кнопки управления миссией:
- старт — запуск кода с предыдущего шага (если код запущен — кнопка недоступна);
- стоп — прерывание кода, выполнение посадки;
kill switch— прерывание кода, дизарм дрона.
- Автоматически обновляемая 2D-карта с возможностью отображения начала координат и каждого найденного здания (квадрат с учетом цвета их крыши).
- Автоматически обновляемый список найденных зданий с отображением цвета, координат, типа (административные здания имеют красные крыши, лаборатории — зеленые, вход в шахту — желтый, здания для обогащения угля — синие).
| № | Критерий | Балл за 1 случай | Количество случаев | Сумма |
|---|---|---|---|---|
| 1 | Установка образа симулятора Gazebo или ПО на систему | 2 | 1 | 2 |
| 2 | Подготовка bash/python скрипта для настройки симулятора | |||
| 2.1 | Настройка clover.launch |
1 | 1 | 1 |
| 2.2 | Настройка aruco.launch |
1 | 1 | 1 |
| 3 | Подготовка bash/python скрипта для случайной генерации мира | |||
| 3.1 | Использование модели зданий из репозитория https://github.com/bart02/dronepoint | 1 | 1 | 1 |
| 3.2 | Установка зданий в количестве 5 шт. | 0,4 | 5 | 2 |
| 3.3 | Минимальное расстояние между центрами зданий 1 м | 1 | 1 | 1 |
| 3.4 | Каждое здание находится в пределах \(x \in [0{,}9]\), \(y \in [0{,}9]\) | 0,2 | 5 | 1 |
| 3.5 | Случайная генерация координат и цвета каждого здания | 1 | 1 | 1 |
| 4 | Подготовка ROS-ноды для выполнения полета и выполнения сканирования | |||
| 4.1 | Сканирование зданий с определением координат и цвета крыши | 0,5 | 10 | 5 |
| 4.2 | Ограничение на количество взлетов и посадок — 1 | 2 | 1 | 2 |
| 4.3 | Возврат на старт после выполнения сканирования | 2 | 1 | 2 |
| 4.4 | Запись результатов сканирования в топик (/buildings) |
2 | 1 | 2 |
| 5 | Разработка веб-приложения с графическим интерфейсом для управления миссией | |||
| 5.1 | Реализация кнопок управления миссией (старт, пауза, стоп, kill switch) |
1 | 3 | 3 |
| 5.2 | Автоматически обновляемая 2D-карта с координатами и цветом зданий | 3 | 1 | 3 |
| 5.3 | Список найденных зданий с отображением цвета, координат и типа здания | 3 | 1 | 3 |
| ИТОГО | 30 | |||
import os
import shutil
import sys
# Карта ArUco в виде строки
aruco_map = '''# id length x y z rot_z rot_y rot_x
0 0.22 0.0 0.0 0 0 0 0
1 0.22 1.0 0.0 0 0 0 0
2 0.22 2.0 0.0 0 0 0 0
3 0.22 3.0 0.0 0 0 0 0
4 0.22 4.0 0.0 0 0 0 0
5 0.22 5.0 0.0 0 0 0 0
6 0.22 6.0 0.0 0 0 0 0
7 0.22 7.0 0.0 0 0 0 0
8 0.22 8.0 0.0 0 0 0 0
9 0.22 9.0 0.0 0 0 0 0
10 0.22 0.0 1.0 0 0 0 0
...
99 0.22 9.0 9.0 0 0 0 0
'''
def replace_string(file_path, old_string, new_string):
"""
Заменяет строку в файле.
Аргументы:
file_path (str): Путь к файлу, который нужно изменить.
old_string (str): Строка, которую нужно найти в файле.
new_string (str): Строка, на которую нужно заменить найденную строку.
"""
with open(file_path, 'r') as file:
lines = file.readlines()
for index, line in enumerate(lines):
if old_string in line:
lines[index] = new_string + '\n' # Заменяем строку
break
with open(file_path, 'w') as file:
file.writelines(lines) # Записываем изменения в файл
# Определение имени пользователя и пути
if len(sys.argv) > 1:
if len(sys.argv) > 2:
directory_name = ' '.join(sys.argv[1:])
else:
directory_name = sys.argv[1]
user_name = directory_name.split('/')[0]
else:
directory_name = 'clover/Desktop'
user_name = 'clover'
# Сохраняем карту ArUco в файл
with open('Nto.txt', 'w') as file:
file.write(aruco_map)
# Проверка и копирование карты ArUco
if 'Nto.txt' not in os.listdir(f"/home/{user_name}/catkin_ws/src/clover/aruco_pose/map"):
shutil.copy('Nto.txt', f"/home/{user_name}/catkin_ws/src/clover/aruco_pose/map")
# Проверка наличия моделей в директории .gazebo
if 'models' in os.listdir(f"/home/{user_name}/.gazebo/"):
for model in os.listdir(f"/home/{user_name}/.gazebo/models"):
# Удаляем ненужные модели
if model in ['aruco_Nto_txt', 'dronepoint_blue', 'dronepoint_green', 'dronepoint_red', 'dronepoint_yellow']:
shutil.rmtree(f"/home/{user_name}/.gazebo/models/{model}")
continue
# Копирование моделей в директорию .gazebo
for model_folder in os.listdir(f"/home/{directory_name}/II_stage_KZ/models"):
shutil.copytree(f'/home/{directory_name}/II_stage_KZ/models/{model_folder}', f"/home/{user_name}/.gazebo/models/{model_folder}", dirs_exist_ok=True)
else:
shutil.copytree(f'/home/{directory_name}/II_stage_KZ/models', f"/home/{user_name}/.gazebo/models")
# Изменение строк в launch файле для запуска ArUco
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="aruco_detect"', ' <arg name="aruco_detect" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="aruco_map"', ' <arg name="aruco_map" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="aruco_vpe"', ' <arg name="aruco_vpe" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="placement"', ' <arg name="placement" default="floor"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="length"', ' <arg name="length" default="0.22"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/aruco.launch', '<arg name="map"', ' <arg name="map" default="Nto.txt"/>')
# Изменение строк в launch файле для запуска Clover
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="fcu_conn"', ' <arg name="fcu_conn" default="usb"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="fcu_ip"', ' <arg name="fcu_ip" default="127.0.0.1"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="fcu_sys_id"', ' <arg name="fcu_sys_id" default="1"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="gcs_bridge"', ' <arg name="gcs_bridge" default="tcp"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="web_video_server"', ' <arg name="web_video_server" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="rosbridge"', ' <arg name="rosbridge" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="main_camera"', ' <arg name="main_camera" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="optical_flow"', ' <arg name="optical_flow" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="aruco"', ' <arg name="aruco" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="rangefinder_vl53l1x"', ' <arg name="rangefinder_vl53l1x" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="led"', ' <arg name="led" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="blocks"', ' <arg name="blocks" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="rc"', ' <arg name="rc" default="false"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="force_init"', ' <arg name="force_init" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover/launch/clover.launch', '<arg name="simulator"', ' <arg name="simulator" default="false"/>')
# Изменение строк в launch файле для симулятора
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="type"', ' <arg name="type" default="gazebo"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="mav_id"', ' <arg name="mav_id" default="0"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="est"', ' <arg name="est" default="ekf2"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="vehicle"', ' <arg name="vehicle" default="clover"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="main_camera"', ' <arg name="main_camera" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="maintain_camera_rate"', ' <arg name="maintain_camera_rate" default="false"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="rangefinder"', ' <arg name="rangefinder" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="led"', ' <arg name="led" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="gps"', ' <arg name="gps" default="false"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="use_clover_physics"', ' <arg name="use_clover_physics" default="false"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="gui"', ' <arg name="gui" default="true"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="world_name" value="$(find clover_simulation)', ' <arg name="world_name" value="$(find clover_simulation)/resources/worlds/Nto.world"/>')
replace_string(f'/home/{user_name}/catkin_ws/src/clover/clover_simulation/launch/simulator.launch', '<arg name="verbose"', ' <arg name="verbose" value="true"/>')
Код полета. Программа на языке Python: import rospy
import cv2
import math
import os
import sys
import pickle
import numpy as np
from sensor_msgs.msg import Image, CameraInfo
from std_msgs.msg import String
from geometry_msgs.msg import PointStamped, Point
from cv_bridge import CvBridge
from clover import srv
from std_srvs.srv import Trigger
import tf2_ros
import tf2_geometry_msgs
import image_geometry
from pymavlink import mavutil
from mavros_msgs.srv import CommandLong
from mavros_msgs.msg import State
# Инициализация ROS-ноды
rospy.init_node('cv', disable_signals=True)
# Прокси-сервисы
get_telemetry = rospy.ServiceProxy('get_telemetry', srv.GetTelemetry)
navigate = rospy.ServiceProxy('navigate', srv.Navigate)
set_attitude = rospy.ServiceProxy('set_attitude', srv.SetAttitude)
land = rospy.ServiceProxy('land', Trigger)
send_command = rospy.ServiceProxy('mavros/cmd/command', CommandLong)
# Публикаторы
buildings_pub = rospy.Publisher('/buildings', String, queue_size=1)
# Настройка TF2
tf_buffer = tf2_ros.Buffer()
tf_listener = tf2_ros.TransformListener(tf_buffer)
camera_model = image_geometry.PinholeCameraModel()
camera_model.fromCameraInfo(rospy.wait_for_message('main_camera/camera_info', CameraInfo))
# Мост для конвертации изображений OpenCV
bridge = CvBridge()
# Цветовые коды и названия моделей зданий
color_text = {
'red': '\033[31m\033[1m',
'green': '\033[32m\033[1m',
'yellow': '\033[33m\033[1m',
'cyan': '\033[36m\033[1m',
'white': '\033[37m\033[1m',
'blue': '\033[34m\033[1m'
}
colors = {
'red': (0, 0, 255),
'green': (0, 255, 0),
'yellow': (0, 255, 255),
'blue': (255, 0, 0)
}
model_name = {
'red': 'Найдено административное здание',
'green': 'Найдена лаборатория',
'yellow': 'Найден вход в шахту',
'blue': 'Найдено здание для обогащения угля'
}
# Инициализация переменных
buildings = ''
count_models = 1
length = 86.7
opr_models = {1: [], 2: [], 3: [], 4: [], 5: []}
img_map = None
# Попытка загрузить изображение карты
try:
img_map = cv2.imread('static/image.png')
except FileNotFoundError:
pass
def real_round(number: float, poz: int = 0) -> float:
"""
Округляет число с плавающей запятой до заданной точности.
Аргументы:
number (float): Число для округления.
poz (int): Количество знаков после запятой.
Возвращает:
float: Округленное число.
"""
a = number * 10 * 10 ** poz
return math.floor(a / 10) / 10 ** poz if a % 10 < 5 else math.ceil(a / 10) / 10 ** poz
def check_position(x: float, y: float, color: str) -> bool:
"""
Проверяет, является ли позиция уникальной (не слишком близкой к уже найденным моделям).
Аргументы:
x (float): Координата X.
y (float): Координата Y.
color (str): Цвет объекта.
Возвращает:
bool: True, если позиция уникальна, иначе False.
"""
models = filter(lambda model: len(model) == 3 and model[2] == color, opr_models.values())
for model in models:
x0, y0, _ = model
if (x0 - x) ** 2 + (y0 - y) ** 2 <= 1:
return False
return True
def land_wait():
"""
Ожидает, пока дрон не приземлится и не будет разармен.
"""
land()
while get_telemetry().armed:
rospy.sleep(0.2)
rospy.loginfo(f'{color_text["yellow"]}[DISARM]{color_text["white"]}')
def navigate_wait(x: float = 0, y: float = 0, z: float = 0, yaw: float = float('nan'), speed: float = 0.6,
frame_id: str = 'aruco_map', auto_arm: bool = False, tolerance: float = 0.2):
"""
Навигация дрона в указанную точку и ожидание достижения цели.
Аргументы:
x (float): Координата X цели.
y (float): Координата Y цели.
z (float): Координата Z (высота).
yaw (float): Угол поворота дрона.
speed (float): Скорость дрона.
frame_id (str): Система координат.
auto_arm (bool): Нужно ли армировать дрон.
tolerance (float): Допустимое отклонение от цели.
"""
navigate(x=x, y=y, z=z, yaw=yaw, speed=speed, frame_id=frame_id, auto_arm=auto_arm)
if auto_arm:
rospy.loginfo(f'{color_text["yellow"]}[ARM]{color_text["white"]}')
rospy.loginfo(f'Takeoff by {color_text["green"]}Body{color_text["white"]} Z = {z}')
else:
rospy.loginfo(f'Flight to {color_text["cyan"]}{frame_id.capitalize()}{color_text["white"]}')
while not rospy.is_shutdown():
try:
telem = get_telemetry(frame_id='navigate_target')
except rospy.ServiceException:
continue
if math.sqrt(telem.x ** 2 + telem.y ** 2 + telem.z ** 2) < tolerance:
break
rospy.sleep(0.2)
def img_xy_to_point(xy: tuple, dist: float) -> Point:
"""
Преобразует координаты пикселей на изображении в 3D мировые координаты.
Аргументы:
xy (tuple): Координаты пикселя (X, Y).
dist (float): Расстояние от камеры до объекта.
Возвращает:
Point: 3D-координаты объекта.
"""
xy_rect = camera_model.rectifyPoint(xy)
ray = camera_model.projectPixelTo3dRay(xy_rect)
return Point(x=ray[0] * dist, y=ray[1] * dist, z=dist)
def get_center_of_mass(mask: np.ndarray) -> tuple:
"""
Вычисляет центр масс бинарной маски.
Аргументы:
mask (np.ndarray): Бинарная маска, где пиксели, принадлежащие объекту, равны 1.
Возвращает:
tuple: Координаты центра масс (X, Y).
"""
M = cv2.moments(mask)
if M['m00'] == 0:
return None
return M['m10'] // M['m00'], M['m01'] // M['m00']
def color_coord(mask: np.ndarray, color_name: str, msg) -> None:
"""
Обрабатывает найденные цветные объекты, вычисляет их координаты и обновляет карту и лог.
Аргументы:
mask (np.ndarray): Бинарная маска цвета.
color_name (str): Название цвета объекта.
msg: Сообщение с заголовком и данными.
"""
global count_models, buildings, img_map
xy = get_center_of_mass(mask)
if xy is None:
return
try:
altitude = get_telemetry('terrain').z
except rospy.ServiceException:
return
xy3d = img_xy_to_point(xy, altitude)
target = PointStamped(header=msg.header, point=xy3d)
try:
setpoint = tf_buffer.transform(target, 'aruco_map', timeout=rospy.Duration(0.2))
except rospy.ServiceException as e:
rospy.logerr(f'Transform error: {e}')
return
if check_position(setpoint.point.x, setpoint.point.y, color_name):
buildings += f'{model_name[color_name]}: X = {real_round(setpoint.point.x, 2)} Y = {real_round(setpoint.point.y, 2)} color = {color_name}\n'
with open('static/otchet.txt', 'a') as file:
file.write(f'{model_name[color_name]}:\n X = {real_round(setpoint.point.x, 2)} Y = {real_round(setpoint.point.y, 2)} color = {color_name}\n\n')
rospy.loginfo(f'{color_text["red"]}[DEBUG]: {model_name[color_name]}: {color_text["yellow"]}{color_name} {color_text["white"]}X = {real_round(setpoint.point.x, 2)} Y = {real_round(setpoint.point.y, 2)}')
opr_models[count_models] = [setpoint.point.x, setpoint.point.y, color_name]
with open('static/coord_model.pkl', 'wb') as file_pkl:
pickle.dump(opr_models, file_pkl)
x, y, color = opr_models[count_models]
bgr_color = colors[color]
cv2.rectangle(img_map,
(108 + int(real_round((x - 0.3) * length)), 892 - int(real_round((y - 0.3) * length))),
(112 + int(real_round((x + 0.3) * length)), 888 - int(real_round((y + 0.3) * length))),
(0, 0, 0), -1)
cv2.rectangle(img_map,
(110 + int(real_round((x - 0.3) * length)), 890 - int(real_round((y - 0.3) * length))),
(110 + int(real_round((x + 0.3) * length)), 890 - int(real_round((y + 0.3) * length))),
bgr_color, -1)
cv2.putText(img_map, f'color = {color_name}',
(90 + int(real_round((x - 0.3) * length)), 905 - int(real_round((y - 0.3) * length))),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 5)
cv2.putText(img_map, f'color = {color_name}',
(90 + int(real_round((x - 0.3) * length)), 905 - int(real_round((y - 0.3) * length))),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, bgr_color, 2)
cv2.putText(img_map, f'X = {real_round(setpoint.point.x, 2)}',
(100 + int(real_round((x - 0.3) * length)), 920 - int(real_round((y - 0.3) * length))),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 5)
cv2.putText(img_map, f'X = {real_round(setpoint.point.x, 2)}',
(100 + int(real_round((x - 0.3) * length)), 920 - int(real_round((y - 0.3) * length))),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, bgr_color, 2)
cv2.putText(img_map, f'Y = {real_round(setpoint.point.y, 2)}',
(100 + int(real_round((x - 0.3) * length)), 935 - int(real_round((y - 0.3) * length))),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 5)
cv2.putText(img_map, f'Y = {real_round(setpoint.point.y, 2)}',
(100 + int(real_round((x - 0.3) * length)), 935 - int(real_round((y - 0.3) * length))),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, bgr_color, 2)
cv2.imwrite('static/image.png', img_map)
count_models += 1
shutdown_initiated = True
@long_callback
def image_callback(msg):
"""
Обрабатывает входящее изображение от камеры, определяет объекты по цвету
и обновляет карту с координатами.
Аргументы:
msg: Сообщение с изображением от камеры.
"""
global buildings, shutdown_initiated
if shutdown_initiated:
return
img = bridge.imgmsg_to_cv2(msg, 'bgr8')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
blue = cv2.inRange(hsv, (115, 150, 150), (125, 255, 255))
green = cv2.inRange(hsv, (55, 150, 150), (65, 255, 255))
red = cv2.inRange(hsv, (0, 150, 150), (5, 255, 255))
yellow = cv2.inRange(hsv, (25, 150, 150), (35, 255, 255))
if cv2.countNonZero(blue) > 10:
color_coord(mask=blue, color_name='blue', msg=msg)
elif cv2.countNonZero(green) > 10:
color_coord(mask=green, color_name='green', msg=msg)
elif cv2.countNonZero(red) > 10:
color_coord(mask=red, color_name='red', msg=msg)
elif cv2.countNonZero(yellow) > 10:
color_coord(mask=yellow, color_name='yellow', msg=msg)
if buildings:
buildings_pub.publish(data=buildings)
rospy.sleep(0.3)
def main():
"""
Основная функция для навигации дрона и выполнения задач.
"""
global count_models, shutdown_initiated, img_map, opr_models
# Проверяем, если есть сохраненные данные о стадии или координатах моделей
if os.path.exists('static/stage.pkl'):
with open('static/stage.pkl', 'rb') as file:
stage_nav = pickle.load(file)
if os.path.exists('static/coord_model.pkl'):
with open('static/coord_model.pkl', 'rb') as file_pkl:
opr_models = pickle.load(file_pkl)
else:
stage_nav = 0
img_map = cv2.resize(bridge.imgmsg_to_cv2(rospy.wait_for_message('aruco_map/image', Image), 'bgr8'), (1000, 1000))
cv2.imwrite('static/image.png', img_map)
try:
os.remove('static/otchet.txt')
except FileNotFoundError:
pass
with open('static/otchet.txt', 'a') as file:
file.write('\n')
# Навигация к стартовой точке
navigate_wait(z=1.5, frame_id='body', auto_arm=True, speed=0.5)
shutdown_initiated = False
for stage, aruco_id in enumerate([9, 19, 10, 20, 29, 39, 30, 40, 49, 59, 50, 60, 69, 79, 70, 80, 89, 99, 90][stage_nav:], start=stage_nav):
navigate_wait(z=1.5, frame_id=f'aruco_{aruco_id}')
with open('static/stage.pkl', 'wb') as file:
pickle.dump(stage + 1, file)
# Возврат к начальной точке
navigate_wait(z=1.5, frame_id='aruco_0', speed=0.7, tolerance=0.3, yaw=0)
navigate_wait(z=0.2, frame_id='aruco_0', speed=0.3, tolerance=0.15, yaw=0)
land_wait()
# Очистка временных файлов
os.remove('static/stage.pkl')
os.remove('static/coord_model.pkl')
shutdown_initiated = True
rospy.sleep(0.3)
exit()
if __name__ == "__main__":
# Запуск программы
if '--land' not in sys.argv and '--kill' not in sys.argv:
main()
rospy.spin()
elif '--land' in sys.argv and '--kill' not in sys.argv:
land_wait()
rospy.loginfo(f'{color_text["red"]}[LAND]{color_text["white"]}')
exit()
elif '--kill' in sys.argv and '--land' not in sys.argv:
if get_telemetry().armed:
set_attitude(thrust=0)
else:
rospy.loginfo(f'{color_text["red"]}Дрон не заармлен{color_text["white"]}')
rospy.loginfo(f'{color_text["red"]}[KILL]{color_text["white"]}')
exit()
Веб-приложение. Код решения: <!DOCTYPE html>
<html>
<head>
<title>Control Panel</title>
<style>
body {
background-color: #1c1c1c;
color: #f0f0f0;
font-family: Arial, sans-serif;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
/* Основной контейнер */
.container {
display: flex;
flex-direction: row;
align-items: center;
gap: 20px;
width: 95%;
justify-content: space-between;
}
/* Кнопки*/
.button-container {
display: flex;
flex-direction: column;
gap: 70px;
align-items: flex-start;
margin-left: 100px;
}
/* Вид кнопок */
.rounded-button {
padding: 35px 70px;
font-size: 32px;
color: #fff;
background-color: #333;
border: none;
border-radius: 20px;
cursor: pointer;
width: 350px;
text-align: center;
transition: all 0.3s ease;
}
/* Анимация наведения на кнопку*/
.rounded-button:not(:disabled):hover {
background-color: #555;
transform: scale(1.1);
}
/* Вид неактивных кнопок */
.rounded-button:disabled {
background-color: #666;
cursor: not-allowed;
opacity: 0.5;
}
/* Изображения */
.image-container img {
max-width: 800px;
border-radius: 20px;
border: 5px solid #333;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
margin-right: 100px;
}
/* Текстовое окно для buildings */
.text-container {
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
margin-left: 10px;
margin-right: 10px;
}
/* Вид этого же окна*/
.text-box {
padding: 10px;
font-size: 20px;
font-weight: bold;
color: #fff;
background-color: #333;
border: none;
border-radius: 20px;
width: 350px;
height: 440px;
overflow-y: auto;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
white-space: pre-wrap;
text-align: center;
}
/* Подпись */
.signature {
position: absolute;
bottom: 10px;
left: 10px;
font-size: 15px;
color: #888;
}
</style>
</head>
<body>
<div class="container">
<div class="button-container">
<button class="rounded-button" id="start">START</button>
<button class="rounded-button" id="stop" disabled>STOP</button>
<button class="rounded-button" id="kill">KILL SWITCH</button>
</div>
<div class="text-container">
<div class="text-box" id="buildings-text"></div>
</div>
<div class="image-container">
<img src="image.png" alt="Control Image" />
</div>
<div class="signature">by Obradovich Vlad</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const startButton = document.getElementById("start");
const stopButton = document.getElementById("stop");
const killButton = document.getElementById("kill");
const imageElement = document.querySelector(".image-container img");
const buildingsTextElement = document.getElementById("buildings-text");
const baseUrl = "/api";
const updateButtons = (response) => {
startButton.disabled = !response.start;
stopButton.disabled = !response.stop;
killButton.textContent = `KILL SWITCH`;
};
const sendRequest = async (button) => {
try {
const response = await fetch(`${baseUrl}/${button}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
updateButtons(data);
} catch (error) {
console.error("Error:", error);
}
};
startButton.addEventListener("click", () => {
if (!startButton.disabled) sendRequest("start");
});
stopButton.addEventListener("click", () => {
if (!stopButton.disabled) sendRequest("stop");
});
killButton.addEventListener("click", () => sendRequest("kill"));
// Получение начального состояния
fetch(`${baseUrl}/state`)
.then((response) => response.json())
.then(updateButtons)
.catch((error) => console.error("Error:", error));
// Функция для обновления изображения
const updateImage = () => {
const timestamp = new Date().getTime();
imageElement.src = `image.png?t=${timestamp}`;
};
// Обновление изображения каждые 0.2 сек
setInterval(updateImage, 200);
// Функция для обновления текста
const updateVariableText = async () => {
try {
const response = await fetch(`${baseUrl}/buildings_text`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
buildingsTextElement.innerHTML = data.text.replace(/\n/g, "<br>");
} catch (error) {
console.error("Error:", error);
}
};
// Обновление текста каждые 0.2 сек
setInterval(updateVariableText, 200);
});
</script>
</body>
</html>













