PyWinAuto + Maya 3D — записки начинающего автоматизатора

от автора

Почему важно узнавать подробности до старта работы

Появилась задачка: взять примерно сто тридцать шотов, настроить в них освещение, пофиксить проблемы при наличии, отправить на рендер. Софт — Autodesk Maya, а каждый шот представляет из себя отдельный файл с анимацией и всеми пирогами. И так двадцать пять раз, потому что двадцать пять эпизодов.

Когда я брался за задачку, наивно посчитал, что можно выкатывать эпизод в месяц. Примерно те же тайминги озвучили заказчики.

Жестоко ошибся: сначала стоило выяснить рабочую процедуру, благо есть у кого.
Выяснив её, я пришёл в ужас. По всему выходило, что на один эпизод улетает от полутора месяцев.

Как жить, когда ты уже не узнал подробностей

Поборов в себе судорожное желание найти новых заказчиков, сел думать, что можно сделать в этих обстоятельствах.

Я задокументировал процесс, и расписал временные ресурсы.

На множестве операций 3D-артист простаивает. Для примера, одна лишь загрузка Maya занимает минуту-две.

Звучит смешно, понимаю. Смешно перестаёт быть при взгляде на эту табличку:

109 часов только на открытие! Это почти месяц.
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
Запуск 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, пушто дерево элементов Майи очень ветвистое.
На иллюстрации Notepad, пушто дерево элементов Майи очень ветвистое.

Нет нужды выводить элементы всего окошка. Стоит выбрать только интересующую область.

Ну а кроме того, можно настроить глубину раскопок:

# так в вывод уйдут только первые две ступени dlg["File"].print_control_identifiers(depth = 2)
На иллюстрации Notepad, пушто дерево элементов Майи очень ветвистое.
На иллюстрации Notepad, пушто дерево элементов Майи очень ветвистое.

Этих знаний достаточно для натыка по Maya 3D, а так — возможности PyWinAuto гораздо шире.

Натыкиваем операции, вставляем между ними проверку потребления ресурсов, и отпускаем скрипт в свободное плавание!

Напоследок

При массовой обработке файлов очень, очень стоит заворачивать конструкции в Try/Except: обидно узнать, что скрипт крашнулся в два часа ночи, и всю ночь машинка стояла без дела.

Проект бодро ползёт к финалу.

Кроме процессов, мы оптимизировали сцены, а это тоже славно режет время на обработку.

Хочу сказать спасибо разработчику PyWinAuto, потому что без него яб лёг и умер.


ссылка на оригинал статьи https://habr.com/ru/post/675264/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *