Skip to main content

tello_integration.py

A seguinte página tem a intenção de descrever as funções principais do código de controle tello_integration.py.

Variáveis importantes

SIMULATION = False # False se estivermos rodando o script no Tello.
BATTERY = 65 # Valor usado para simulação
# ---------------------------- #

INTERFACE_FACTOR = 1 # Increase for better resolution monitor
HAND_SIZE = 1 # Decrease for smaller hands

Classe Drone

init()

Tem a intenção de inicializar Mediapipe (mp) e variáveis. Além disso, chama a função startup que se conecta com o Tello, permitindo printar também seu estado (bateria, altitude, velocidade, etc).

def __init__(self):

self.tello_startup()
# Inicializando Mediapipe
self.mp_drawing = mp.solutions.drawing_utils
self.mp_drawing_styles = mp.solutions.drawing_styles
self.mp_hands = mp.solutions.hands

self.state = self.tello.get_current_state()
print(self.state)

self.tricks = True # Assumindo que bateria está acima de 53%

if self.tello.get_height() == 0: #Importante para caso rodemos o código com o Tello já decolado
self.height = 0
self.takeoff = False
else:
self.height = self.tello.get_height()
self.takeoff = True

# Inicializando variáveis
self.prev_vector = [0, 0, 0, 0, 0]
self.repeat = 0
...
self.num_repeat = 30 # Mínimo para afirmar que foi feito um gesto

tello_startup()

Essa função tem o intuito de conectar o Tello, permitindo rodar o script. Para isso, primeiro, cria-se um objeto da classe Tello (biblioteca DJI). Depois, conectamos com o drone e iniciamos o stream da câmera.

# Inicialização do Tello
def tello_startup(self):
# For Tello input:
self.tello = Tello() # Starts the tello object
self.tello.connect() # Connects to the drone
self.tello.streamon()

get_tello_battery()

Função que pega a bateria do Tello e atualiza a variável self.tricks. Essa variável é Falsa, caso a bateria esteja abaixo de 53% (margem de erro de 3%), previnindo que o Tello pouse automaticamente ao tentar dar flips (só funcionam acima de 50% de bateria). O intuito é chamar essa função constantemente para atualizar sempre se é possível ou não dar um flip.

# Atualizar valor da bateria
def get_tello_battery(self):
if not SIMULATION:
self.battery = self.tello.get_battery()
else:
self.battery = BATTERY

if self.battery <= 53:
self.tricks = False
else:
self.tricks = True

tello_no()

Função que faz o Tello rotacionar como se estivesse dizendo "Não". É chamada quando a bateria está abaixo de 53%, no caso, e ele recebeu o comando de dar flip.

# Negar truques por bateria insuficiente
def tello_no(self):
print(f"Bateria insuficiente! {self.battery}")
if not SIMULATION:
self.tello.rotate_clockwise(30)
self.tello.rotate_counter_clockwise(60)
self.tello.rotate_clockwise(30)

return_to_pos()

Função que faz o Tello retornar para posição inicial (estimada) após dar um flip. O Tello pode ser instável ao fazer truques, principalmente se estiver em espaço aberto com vento. A intenção é fazer ele se mover para um sentido dependendo da orientação do flip dado.

# Voltar para a posicao que estava antes do flip dependendo da orientação que é passada como parâmetro
def return_to_pos(self, orientation):

if orientation == 'back':
print("moving foward...")
if not SIMULATION:
self.tello.move_forward(40)
elif ...

keep_tello_alive()

Função que manda um sinal para o Tello, evitando o pouso automático que ocorre se ele não recebe comandos em 15 segundos.

def keep_tello_alive(self):
# Manda sinal para o tello não pousar. Criamos uma função, pois chamaremos em outra classe
if not SIMULATION:
self.tello.send_control_command("command")

hand_keyboard_control()

Função que controla o Tello a partir de gestos e do teclado, simultaneamente. Na interface principal, isso é acessado apertando 1.

Ver o frame da câmera

Para acessar cada frame do Tello:

self.image = self.tello.get_frame_read().frame  # Stores the current streamed frame

Não deixar o Tello pousar

Para não pousar o drone automaticamente, em loops, é interessante colocar para mandar um sinal a cada 10 segundos.

if datetime.now() - self.start > timedelta(seconds=10):
self.keep_tello_alive()
self.start = datetime.now()

Detectar mãos no Tello e a mais perto da câmera

results = hands.process(self.image)

A próxima parte do código verifica se a mão identificada é A maior no frame. Fazemos isso calculando a área do triângulo formado entre os pontos 0, 5 e 17:

Mediapipe

A tolerância do tamanho pode ser redefinida no início do código, nas variáveis importantes (HAND_SIZE).

# área do triângulo central
# lm = landmark
area = 1/2 * abs(lm0.x * (lm5.y - lm17.y) + lm5.x * (lm17.y - lm0.y) + lm17.x * (lm0.y - lm5.y)) / self.abs_area
if area > larger:
true_hand = potential_hand
larger = area

Acima, vemos que se a área for grande o suficiente (maior que a maior mão anterior), a nova "mão verdadeira" é a atual e a tolerância foi redefinida para a área dessa mão nova.

Verificar posição de cada dedo

Em seguida, caso haja uma "mão verdadeira", ou seja, perto o suficiente, cria-se um vetor de dedos. Esse será modificado caso o dedo esteja levantado ("1") ou abaixado ("0").

# Inicializa vetor de dedos
finger_vector = [0, 0, 0, 0, 0]

A ideia para verificar se o dedo está abaixado ou não é traçar vetores entre os landmarks. Veja o exemplo abaixo do polegar:

dist0 = ( (marks[5].x - marks[17].x)**2 + (marks[5].y - marks[17].y)**2 )**(1/2)  # Distância entre pontos 5 e 17

# Tolerância (Quanto menor, mais conservadora é a detecção)
TOL = 1.5 # Polegar

# Compara distâncias entre pontos para decidir quais dedos estão abertos ou fechados:

# polegar
finger_dist1 = ( (marks[4].x - marks[17].x)**2 + (marks[4].y - marks[17].y)**2 )**(1/2)
if finger_dist1 < TOL*dist0:
finger_vector[0] = 0
else:
finger_vector[0] = 1

Note que o que ele faz é basicamente verificar se o polegar está para fora da mão ou para dentro (fechado), dependendo dos landmarks 4, 5 e 17. E a tolerância disso é um pouco maior (1.5). Veja um teste no modo SIMULATION:

Polegar

No momento em que mudamos para o polegar para fora, o código identifica como "flip right" (direita do drone). Mas como ele percebe a orientação do polegar? Como todo vetor tem direção e sentido, podemos também verificar se o polegar aponta para a direita ou para esquerda, vendo se o landmark 4 está com abscissa maior ou menor que landmark 17:

# Orientação do polegar no eixo X
if marks[4].x - marks[17].x < 0:
self.orientation_x = 'right'
else:
self.orientation_x = 'left'

A partir disso, é possível aplicar a mesma lógica para os outros dedos, utilizando landmarks que façam sentido. Uma estratégia é sempre comparar a distância do landmark 9 e 0 com a distância do ponto no extremo de cada dedo e do landmark 0. Um exemplo com o indicador:

Indicador

Veja que se a distância azul for maior que a vermelha, o indicador está levantado. Caso contrário, ele está abaixado. O primeiro vetor seria representado por [1, 1, 1, 1, 1] e o segundo vetor seria [1, 0, 1, 1, 1].

Confirmar gesto por contador de repetição

Para ter certeza que a pessoa está fazendo um gesto, é verificado 30 vezes se é o mesmo gesto sendo identificado. Para seguir a mão, é verificado apenas 10 vezes para tornar mais rápida essa funcionalidade. Veja a estrutura base dessa lógica:

if self.repeat > self.num_repeat: # Se tiver detectado esse gesto várias vezes
self.get_tello_battery() # Atualiza estado da bateria
self.command = self.verify_commands(finger_vector) # Vai ver o que fazer com aquele gesto

# Reiniciar variáveis
self.prev_vector = [0, 0, 0, 0, 0]
self.repeat = 0

# Se ainda não tiver o número mínimo de repetições para seguir a mão
else:
# Mesmo vetor encontrado
if finger_vector == self.prev_vector:
self.repeat += 1
# Se encontrar um vetor diferente
else:
self.repeat = 0
# Vetor atual é o vetor anterior da pŕoxima iteração
self.prev_vector = finger_vector

Identificar comando vindo do teclado

Nessa mesma função, também há uma parte do loop que sempre recebe comandos vindoo do teclado (caso alguma tecla seja apertada). A estrutura base disso é a seguinte:

for event in pg.event.get():

self.get_tello_battery() #Atualiza estado de bateria

if event.type == pg.KEYDOWN:

if event.key == pg.K_w: #Exemplo com a tecla "w"

print("Going foward...")

if not SIMULATION:
self.tello.move_forward(20) # Move o drone para frente


Para colocar comandos em outras teclas é apenas repetir if event.key == pg.K_x: e adicionar uma ação dentro da condição (sendo x uma tecla qualquer).

verify_commands()

Função que vai verificar qual comando foi recebido a partir dos gestos com a mão identificada. Basicamente, comparamos o vetor recebido com o que queremos para mandar um comando para o Tello:

elif vector == [0, 1, 0, 0, 0]: #Vetor com indicador levantado apenas, "1"

if self.tricks and self.orientation_y == 'back':

print("Flip back!")

if not SIMULATION:
self.tello.flip_back()

self.return_to_pos(self.orientation_y) # Volta para posição inicial estimada
else:
self.tello_no() # Não possui bateria para fazer o flip

Os outros comandos seguem a mesma estrutura. O que muda é a ação desejada dentro da condição.

follow_hand()

Essa função tenta seguir a mão identificada anteriormente. Ela é acessada no verify_commands() quando o vetor dos dedos é [1, 1, 1, 1, 1] (mão aberta). Ela recebe como parâmetros as coordenadas do landmark 9 e as ordenadas dos extremos da mão (landmark 12 e 0). Assim, é possível tentar centralizar no frame da câmera o landmark 9, com uma tolerância previamente definida na função. Importante ressaltar que o ponto (0,0) é no canto superior esquerdo e o ponto (1,1) é no canto inferior direito. Veja a estrutura base dessa lógica para uma centralização da mão no eixo X:

P_tol = 0.075 # Quanto maior -> menos responsivo

x_error = (pixel[0] - 0.5) # pixel[0] é a abscissa do landmark 9

if abs(x_error) < P_tol:

x_error = 0 # Ignora, se estiver dentro da tolerância -> importante para não floodar comando para o drone

x_vel = -int(100*x_error)

if not SIMULATION:
mod_x = abs(x_vel) # Arredonda velocidade

if mod_x > 40:
mod_x = 40

if mod_x >= 20:
if x_vel < 0:
self.tello.move_left(mod_x) # Manda o Tello para a esquerda (velocidade negativa)
else:
self.tello.move_right(mod_x)
# Manda o Tello para a direita (velocidade positiva)

A ideia inicial era mandar um comando de self.tello.send_rc_control, definindo uma velocidade para o eixo x, y e z. Entretanto, isso acabava por floodar o Tello de mensagens, inclusive com as três velocidades setadas para 0. Foi decidido então mover o Tello com o comando self.tello.move_left (ou right), dependendo de quão descentralizado estiver o landmark 9. Além disso, o Tello não se move mais para cima ou para baixo. ELe segue a mão apenas no eixo X, por razões de segurança e porque deixa o movimento mais fluido. Entretanto, deixamos uma estrutura no código, caso alguém queira setar alguma velocidade para o Tello no eixo Y. De qualquer forma, ainda adicionamos um limite de altura, para que ele não suba muito, se fóssemos alterar sua posição no eixo Y ao seguir a mão:

if not SIMULATION:
self.height = self.tello.get_height()
if self.height > 200: # Se estiver acima de 2 metros, deve descer
print("Too high, mooving down...")
self.tello.move_down(40)

Para finalizar, colocamos também uma medida de segurança para o Tello se afastar da mão, caso ela esteja muito perto. Isso é feito medindo a distância entre os extremos da mão:

z_error = abs(ext_up - ext_down) # Distância entre extremos da mão

if not SIMULATION:
if z_error > 0.8:
print("Too close! Moving backwards...")
self.tello.move.back(30) #Move o Tello para trás
else:
z_error = 0

Como a medida do topo até a base do frame é 1, se a distância entre os landmarks 12 e 0 for maior que 0.8, o drone se afasta 30 cm da mão. Close

follow_camera_game()

Função do jogo de câmeras, em que a pessoa deve tentar aparecer no frame do Tello enquanto o drone realiza movimentos de rotação e descida/subida.

O jogo se inicia com o usuário escolhendo um nível de dificuldade, que vai de 1 a 3, assim como o número de fotos que deseja tirar. Tudo isso é obtido pelo input. Basicamente, quanto mais difícil, menor é o intervalo entre as fotos e a pessoa deve se movimentar mais rápido:

time.sleep(5-int(difficulty))

Se o Tello rotaciona, desce ou sobe é aleatório, assim como o quanto que ele se move ou gira:

choices = ["left", "right", "up", "down"]
deg_choices = [30, 40, 50, 60]
dist_choices = [20, 30, 40]
...
choice = random.choice(choices)
...
if choice == "left":
degrees = deg_choices[random.randint(0, 3)]
print("Girando", degrees, "° para a esquerda")
if not SIMULATION:
self.tello.rotate_counter_clockwise(degrees)

A mesma estrutura se aplica às outras opções. Um problema, entretanto, é não mandar o Tello para alturas muito grandes ou muito baixas. Por isso, colocamos um limite minHeight e maxHeight de 170 cm e 20 cm, respectivamente. Assim, se a distância aleatória para subir ou descer distoar desse parâmetros, é mandado o Tello se movimentar no sentido contrário. Veja um exemplo:

if not SIMULATION:
getHeight = self.tello.get_height()
...

elif choice == "down":
distance = dist_choices[random.randint(0, 2)]
if getHeight - distance > minHeight:
print("Descendo", distance, "cm")
if not SIMULATION:
self.tello.move_down(distance)
getHeight -= distance

else: #if it is lower than the min height, move up instead
print("Subindo", distance, "cm")
if not SIMULATION:
self.tello.move_up(distance)
getHeight += distance

Essa escolha aleatória de movimentação acontece dentro de um loop que é repetido para o número de fotos a serem tiradas (que foi escolhido pelo usuário).

 for i in range(num_pictures):

Veja alguns dos resultados :)

Game

Classe Interface

init()

Essa função chama outras funções e cria um objeto do tipo Drone:

def __init__(self):
print("\nBem vindo!")
self.tello = Drone()

self.print_commands()
self.create_window()

create_window()

Função que cria janela da interface com o uso da biblioteca Pygame. Para criar uma janela:

self.f = INTERFACE_FACTOR # AJustando tamanho da janela
self.dim = (500*self.f, 480*self.f)
pg.init()
self.win = pg.display.set_mode((self.dim[0], self.dim[1]))
pg.display.set_caption("Pygame")

Para criar um tipo de texto que posteriormente (em outra função, em loop) será adicionado na janela:

font = pg.font.Font('freesansbold.ttf', int(16+self.f*16))

white = (255, 255, 255)
black = (0, 0, 0)

self.text = font.render('SkyRats - Tello', True, white, black)

A mesma estrutura pode ser repetida para criar outros textos.

Apenas printa os comandos da interface no terminal:

Print

interface_loop()

Loop da interface. Nela é criada a janela e também são recebidos os comandos do teclado para acessar outras funcionalidades do programa, utilizando a mesma estrutura já vista antes na função do hand_keyboard_control(). Aqui também é utilizada a função self.keep_tello_alive() para evitar que o drone pouse automaticamente. Para adicionar textos na janela do Pygame:

d0 = 30*self.f
...
self.win.blit(self.text, (d0, d0))

Após configurar como desejado a janela, você deve chamar pg.display.update() para fazer com que a janela realmente apareça no monitor do usuário:

Interface Pygame