PyWinAuto + Maya 3D — записки начинающего автоматизатора
Почему важно узнавать подробности до старта работы
Появилась задачка: взять примерно сто тридцать шотов, настроить в них освещение, пофиксить проблемы при наличии, отправить на рендер. Софт — Autodesk Maya, а каждый шот представляет из себя отдельный файл с анимацией и всеми пирогами. И так двадцать пять раз, потому что двадцать пять эпизодов.
Когда я брался за задачку, наивно посчитал, что можно выкатывать эпизод в месяц. Примерно те же тайминги озвучили заказчики.
Жестоко ошибся: сначала стоило выяснить рабочую процедуру, благо есть у кого.
Выяснив её, я пришёл в ужас. По всему выходило, что на один эпизод улетает от полутора месяцев.
Как жить, когда ты уже не узнал подробностей
Поборов в себе судорожное желание найти новых заказчиков, сел думать, что можно сделать в этих обстоятельствах.
Я задокументировал процесс, и расписал временные ресурсы.
На множестве операций 3D-артист простаивает. Для примера, одна лишь загрузка Maya занимает минуту-две.
Звучит смешно, понимаю. Смешно перестаёт быть при взгляде на эту табличку:
109 часов только на открытие! Это почти месяц.
Все шесть операций человек делает пару кликов на операцию, а потом задумчиво тупит в монитор. Очень не люблю такой формат работы, он высаживает всё внимание за пару часов.
Оптимизировать пайплайн проблематично: модельки русские, анимация иранская, собирается это всё и вовсе третьей стороной, плагины под проект написаны также иранцами.
Лезть во всё это великолепие руками страшно: во-первых, есть риск заблудиться навсегда, а во-вторых, убрав паузу из какого-нибудь батника можно невзначай всю телегу поломать.
Зато можно имитировать оператора ПК питоном! Я собрал у коллег информацию об ошибках, возникающих в процессе работы. Убедился в том, что большая их часть — типовые, а также не помешают работе скрипта. И начал продумывать план побега, который скостит время простоя оператора.
Первым делом разметил таски двумя цветами: обязательное присутствие мозга при выполнении, и необязательное.
Так я вычленил операции, которые можно безболезненно перевесить на скрипт.
Как решалось
Несколько раз пользовавшись библиотекой PyWinAuto, обратился к ней опять.
Людей на рендере много и у них разные машины, потому координатный подход к автоматизации отмёлся — буду ловить кнопки по названиям.
Поехали
Импортнём нужные библиотеки.
import pywinauto as pw # для работы с GUI
import os # для работы с файлами и папками
from time import sleep # для таймеров
import logging # для ведения лога
import sys # для вывода информации в терминал
from PIL import ImageGrab # для скриншотов
from datetime import datetime # для записи времени в название скриншота
import matplotlib.pyplot as plt # на случай острого желания построить пару графиков
Первое, чего захочется — безудержного логирования! Возможно, даже со скриншотами. Но это не точно, поэтому повесим скриншот на булинь.
def log(file_name, massage, screenshot = False):
logging.basicConfig(
format="%(asctime)s %(message)s",
encoding='utf-8',
level=logging.DEBUG,
handlers=[
logging.FileHandler("log\\" + file_name + '.log'),
logging.StreamHandler(sys.stdout)
])
logging.debug(massage)
if screenshot == True:
now = datetime.now()
current_time = now.strftime("%H_%M_%S_")
myscreen = ImageGrab.grab()
myscreen.save("log\\" + current_time + file_name + '.jpg')
Второе — получить список файлов с явками и паролями. Исходные файлы лежат в отдельной папке рядом со скриптом, так что всё удобно.
def get_files_list():
path = os.getcwd() # подхватываем адрес рабочей папки
files = os.listdir(path + '\\animate') # собираем список на обработку
return(files)
Третье — обработать каждый файлик. Соберу конструкцию, которую дальше буду дополнять.
def process():
files = get_files_list()
for file in files:
open_shot(file)
baking_animation_curves(file)
get_assembly(file)
Откроем файл в лоб через проводник. Допускаю, что есть более простые методы.
def open_shot(file):
pw.Application().start('explorer.exe "C:\\Program Files"') # стартуем проводник
app = pw.Application(backend="uia").connect(path="explorer.exe", title="Program Files") # подключаемся к нему
dlg = app["Program Files"] # подключаемся к окошку
dlg['Address: C:\Program Files'].wait("ready") # ждём готовности поисковой строки
dlg.type_keys("%D") # переключаемся на неё хоткеем
dlg.type_keys(file, with_spaces=True) # ввпечатываем туда файл
dlg.type_keys("{ENTER}")
app.kill() # закрываем проводник
Окно Майи появляется секунд через тридцать после обращения к файлу. Напишем функцию, которая проверит появление окна, и заодно даст доступ скрипту к Майе.
def maya_connect():
try:
# пробуем подключиться к окошку:
app = pw.Application(backend="uia").connect(title_re=".* - Autodesk Maya 2020.4:")
print("Connected!")
return app
# если окошка нет:
except pw.findwindows.ElementNotFoundError:
# Можно взять просто time.sleep(), но так мы
# дадим понять пользователю, что скрипт не висит
for i in ['. ', '.. ', '...']:
sys.stdout.write('\r'"Сonnecting" + i)
sleep(1)
return maya_connect()
Теперь надобно понять, что файл открылся, и с ним можно работать. Вариант повесить на таймер не годится: файлы весят по-разному, с-во берут разное время на загрузку.
PyWinAuto предлагает app.wait_cpu_usage_lower(threshold=5)
— план хорош!
Проверим, как выглядит кривая потребления CPU Майей:
код
def get_app_load (app):
load_points = [1] # список для записи загрузки
# начинается с единицы, чтобы сработал цикл
timer = [0] # список для подсчёта секунд со старта записи нагрузки
n = 0 # счётчик времени
# пока сумма значений нагрузки за последние 60 секунд не станет нулевой,
# собираем значения
while sum(load_points[-60:]) != 0:
load = app.cpu_usage()
if load < 0.5: # убираем случайные всплески
load = 0
load_points.append(load)
n += 1
timer.append(n)
sleep(1)
plt.plot(timer, load_points)
plt.show()
Запуск Maya
Майя дискретно освобождает ресурсы процессора в процессе работы, так что ждать cpu_lower
смысла нет.
Совместим таймер и чек нагрузки на процессор:
def app_status(app):
"""если последние 30 секунд Майя не трогала процессор,
будем считать, что она освободилась"""
load_points = [1]
while sum(load_points[-30:]) != 0:
load = app.cpu_usage()
if load < 0.5: # убираем случайные всплески
load = 0
load_points.append(load)
for i in ['. ', '.. ', '...']:
sys.stdout.write('\r'"Maya is working now" + i)
sleep(0.33)
Будем пользовать эту конструкцию для ожидания конца операций.
Файл открыли, можно теперь и автоматизировать что-нибудь
…предварительно обложившись инструментами. Нам понадобится:
Функция, печатающая дерево элементов в выбранном диалоге:
# для печати в файл
dlg.print_control_identifiers(depth = None, filename = "MayaControls.txt")
# или просто вывода
dlg.print_control_identifiers(depth = None)
И функция, рисующая зелёный прямоугольник вокруг выбранного элемента:
# или не зелёный, тут как настроишь :)
dlg.draw_outline(colour='green')
dlg['ShowMenuItem2'].draw_outline(colour='red')
dlg.MultyDoTask.draw_outline(colour='blue')
Как обращаться к элементам
Диалог — dlg
— это коробка с графическими элементами. А главное окно приложения — тоже диалог, потому обращаться к интересующему можно через dlg = pw.Desktop(backend="uia")["Anything"]
.
Независимо от способа работы с диалогом, через connect()
или Desktop
, понадобится определить, что этот диалог отличает от собратьев. Часто достаточно названия, отображаемого на экране.
Внутри диалога лежит не просто графика, а контроллеры: кнопки, чекбоксы, тому подобное и… другие диалоги.
Подключаемся к диалогу:
# так:
dlg = pw.Desktop(backend="uia")["Untitled - Notepad"]
# либо так:
app = pw.application.Application(backend="uia").connect(title = "Untitled - Notepad")
dlg = app["Untitled - Notepad"]
Затем ищем и что-то делаем с необходимым элементом:
dlg["File"].click_input() # клик по кнопке "File"
Питон шибко быстрее графического интерфейса, а тем более, монструозной софтины. Так что перед совершением действия просим скрипт подождать:
# всё вместе, или любая комбинация:
dlg["File"].wait("exists enabled visible ready active").click_input()
Eсли искомая кнопка в интерфейсе легко находится скриптом по названию — всё ок.
Если находится не та — печатаем идентификаторы, находим уникальное имя кнопки — и всё опять ок.
Дерево идентификаторов может выглядеть странно.
В первую голову надо удостовериться в правильно выбранном бэкенде при подключении к приложению:
На иллюстрации Notepad, пушто дерево элементов Майи очень ветвистое.
Нет нужды выводить элементы всего окошка. Стоит выбрать только интересующую область.
Ну, а кроме того, можно настроить глубину раскопок:
# так в вывод уйдут только первые две ступени
dlg["File"].print_control_identifiers(depth = 2)
На иллюстрации Notepad, пушто дерево элементов Майи очень ветвистое.
Этих знаний достаточно для натыка по Maya 3D, а так — возможности PyWinAuto гораздо шире.
Натыкиваем операции, вставляем между ними проверку потребления ресурсов, и отпускаем скрипт в свободное плавание!
Напоследок
При массовой обработке файлов очень, очень стоит заворачивать конструкции в Try/Except
: обидно узнать, что скрипт крашнулся в два часа ночи, и всю ночь машинка стояла без дела.
Проект бодро ползёт к финалу.
Кроме процессов, мы оптимизировали сцены, а это тоже славно режет время на обработку.
Хочу сказать спасибо разработчику PyWinAuto, потому что без него яб лёг и умер.