402 lines
15 KiB
Python
402 lines
15 KiB
Python
from pathlib import Path
|
|
import time
|
|
import yaml
|
|
import shutil
|
|
import string
|
|
import random
|
|
import trimesh
|
|
from copy import deepcopy
|
|
from typing import Optional
|
|
from lmm_utils.core import MMUA
|
|
# Custom
|
|
from assets.garment_programs.meta_garment import MetaGarment
|
|
from assets.bodies.body_params import BodyParameters
|
|
import pygarment as pyg
|
|
from pygarment.meshgen.boxmeshgen import BoxMesh
|
|
# from pygarment.meshgen.simulation import run_sim
|
|
import pygarment.data_config as data_config
|
|
from pygarment.meshgen.sim_config import PathCofig
|
|
|
|
|
|
|
|
verbose = False
|
|
|
|
def _id_generator(size=10, chars=string.ascii_uppercase + string.digits):
|
|
"""Generate a random string of a given size, see
|
|
https://stackoverflow.com/questions/2257441/random-string-generation-with-upper-case-letters-and-digits
|
|
"""
|
|
return ''.join(random.choices(chars, k=size))
|
|
|
|
|
|
class GUIPattern:
|
|
def __init__(self) -> None:
|
|
# Unique id to distiguish tab sessions correctly
|
|
self.id = _id_generator(20)
|
|
|
|
# Paths setup
|
|
self.save_path_root = Path.cwd() / 'tmp_gui' / 'downloads'
|
|
self.tmp_path_root = Path.cwd() / 'tmp_gui' / 'display'
|
|
self.save_path = self.save_path_root / self.id
|
|
self.svg_filename = None
|
|
self.saved_garment_archive = ''
|
|
self.saved_garment_folder = ''
|
|
self.tmp_path = self.tmp_path_root / self.id
|
|
self.paths_3d = None
|
|
|
|
# create paths
|
|
self.save_path.mkdir(parents=True, exist_ok=True)
|
|
self.tmp_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.body_params = None
|
|
self.design_params = {}
|
|
self.design_list = []
|
|
self.design_sampler = pyg.DesignSampler()
|
|
self.sew_pattern = None
|
|
|
|
self.body_id = 'mean_all'
|
|
self.body_file = None
|
|
self.design_file = None
|
|
self._load_body_file(
|
|
Path.cwd() / f'assets/bodies/{self.body_id}.yaml'
|
|
)
|
|
self.default_body_params = deepcopy(self.body_params)
|
|
self._load_design_file(
|
|
Path.cwd() / 'assets/design_params/default.yaml'
|
|
)
|
|
|
|
# Status
|
|
self.is_self_intersecting = False
|
|
self.is_in_3D = False
|
|
|
|
self.reload_garment()
|
|
# Init Agent
|
|
self.agent=None
|
|
|
|
def parse_chat(self, text_prompt='', img_url='',api_key=None, base_url=None, model=None,text_model=None):
|
|
print('Prompt: ', text_prompt)
|
|
print('Image: ', img_url)
|
|
self.agent.mmua=MMUA(api_key=api_key, base_url=base_url, model=model,text_model=text_model)
|
|
# self.agent=Agent(api_key=api_key, base_url=base_url, model=model,text_model=text_model)
|
|
text_prompt = text_prompt.strip()
|
|
modify_mode = 'modify' in text_prompt or 'm:' in text_prompt
|
|
stress_mode='stress' in text_prompt or 's:' in text_prompt
|
|
|
|
# try:
|
|
# Modify mode
|
|
if modify_mode:
|
|
assert self.design_params and self.design_list, "Must provide a base design under mofify mode."
|
|
gpt_response,gpt_design_params,gpt_design_list=self.agent.modify_design(self.design_list,text_prompt=text_prompt,design_params=self.design_params)
|
|
# Stress mode:
|
|
elif stress_mode:
|
|
assert self.design_params and self.design_list, "Must provide a base design under mofify mode."
|
|
gpt_response,gpt_design_params,gpt_design_list=self.agent.stress_design(self.design_list,img_url=img_url,design_params=self.design_params)
|
|
# Inference from image and text
|
|
elif text_prompt and img_url:
|
|
gpt_response,gpt_design_params,gpt_design_list=self.agent.picture_text_design(img_url,text_prompt)
|
|
|
|
# Inference from text only
|
|
elif text_prompt and not img_url:
|
|
gpt_response,gpt_design_params,gpt_design_list=self.agent.text_design(text_prompt)
|
|
|
|
# Inference from image only
|
|
elif img_url and not text_prompt:
|
|
gpt_response,gpt_design_params,gpt_design_list=self.agent.picture_design(img_url)
|
|
else:
|
|
raise ValueError("At least one design description is required, text prompt or image.")
|
|
|
|
self.design_list = gpt_design_list
|
|
self.set_new_design(gpt_design_params)
|
|
self.design_params=gpt_design_params
|
|
print('*** Design params: ', self.design_list, self.design_params)
|
|
|
|
return gpt_response
|
|
|
|
def release(self):
|
|
"""Clean up tmp files after the session"""
|
|
self.clear_previous_download()
|
|
shutil.rmtree(self.save_path)
|
|
shutil.rmtree(self.tmp_path)
|
|
|
|
def _load_body_file(self, path):
|
|
self.body_file = path
|
|
self.body_params = BodyParameters(path)
|
|
|
|
def _load_design_file(self, path):
|
|
self.design_file = path
|
|
|
|
# Create values
|
|
with open(path, 'r') as f:
|
|
des = yaml.safe_load(f)['design']
|
|
|
|
self.design_params.update(des)
|
|
if 'left' in self.design_params and not self.design_params['left']['enable_asym']['v']:
|
|
self.sync_left()
|
|
|
|
# Update param sampler
|
|
self.design_sampler.load(path)
|
|
|
|
def svg_path(self):
|
|
return self.tmp_path / self.svg_filename
|
|
|
|
def set_new_design(self, design):
|
|
self._nested_sync(design, self.design_params)
|
|
|
|
def set_new_body_params(self, body_params):
|
|
self.body_params.load_from_dict(body_params)
|
|
|
|
def sample_design(self, reload=True):
|
|
"""Random design parameters"""
|
|
|
|
new_design = self.design_sampler.randomize()
|
|
# NOTE: re-assign the values instead up overwriting them
|
|
self._nested_sync(new_design, self.design_params)
|
|
|
|
if 'left' in self.design_params and not self.design_params['left']['enable_asym']['v']:
|
|
self.sync_left()
|
|
|
|
if reload:
|
|
self.reload_garment()
|
|
|
|
def restore_design(self, reload=True):
|
|
"""Restore design values to match the current loaded file"""
|
|
new_design = self.design_sampler.default()
|
|
# re-assign the values instead up overwriting them
|
|
self._nested_sync(new_design, self.design_params)
|
|
|
|
if reload:
|
|
self.reload_garment()
|
|
|
|
def reload_garment(self):
|
|
"""Reload sewing pattern with current body and design parameters
|
|
|
|
NOTE: loading a pattern might be lagging, execute only when needed!
|
|
"""
|
|
self.sew_pattern = MetaGarment(
|
|
'Configured_design', self.body_params, self.design_params)
|
|
self.is_self_intersecting = self.sew_pattern.is_self_intersecting()
|
|
self._view_serialize()
|
|
|
|
@staticmethod
|
|
def _nested_sync(s_from, s_to):
|
|
if 'v' in s_to:
|
|
s_to['v'] = s_from['v']
|
|
else:
|
|
for key in s_to:
|
|
if key in s_from:
|
|
GUIPattern._nested_sync(s_from[key], s_to[key])
|
|
|
|
def sync_left(self, with_check=False):
|
|
"""Synchronize left and right design parameters"""
|
|
# Check if needed in the first place
|
|
if with_check and self.design_params['left']['enable_asym']['v']:
|
|
# Asymmetry enabled, the params should not syncronise
|
|
return
|
|
for k in self.design_params['left']:
|
|
if k != 'enable_asym':
|
|
# Use proper value assignment instead of deepcopy
|
|
self._nested_sync(self.design_params[k], self.design_params['left'][k])
|
|
|
|
def _view_serialize(self):
|
|
"""Save a sewing pattern svg representation to tmp folder be used
|
|
for display"""
|
|
|
|
# Get the flat representation
|
|
pattern = self.sew_pattern.assembly()
|
|
|
|
# Clear up the folder from previous version -- it's not needed any more
|
|
self.clear_previous_svg()
|
|
try:
|
|
self.svg_filename = f'pattern_{time.time()}.svg'
|
|
dwg = pattern.get_svg(self.tmp_path / self.svg_filename,
|
|
with_text=False,
|
|
view_ids=False,
|
|
flat=False,
|
|
margin=0
|
|
)
|
|
dwg.save()
|
|
|
|
self.svg_bbox_size = pattern.svg_bbox_size
|
|
self.svg_bbox = pattern.svg_bbox
|
|
except pyg.EmptyPatternError:
|
|
self.svg_filename = ''
|
|
|
|
# Cleaning
|
|
def clear_previous_svg(self):
|
|
"""Clear previous svg display file"""
|
|
if self.svg_filename:
|
|
(self.tmp_path / self.svg_filename).unlink()
|
|
self.svg_filename = ''
|
|
|
|
def clear_previous_download(self):
|
|
"""Clear previous download package display file"""
|
|
if self.saved_garment_folder:
|
|
shutil.rmtree(self.saved_garment_folder)
|
|
self.saved_garment_folder = ''
|
|
if self.saved_garment_archive:
|
|
self.saved_garment_archive.unlink()
|
|
self.saved_garment_archive = ''
|
|
|
|
def clear_3d(self):
|
|
if self.paths_3d is not None:
|
|
shutil.rmtree(self.paths_3d.out_el)
|
|
self.paths_3d = None
|
|
|
|
# 3D
|
|
def drape_3d(self):
|
|
"""Run the draping of the current frame"""
|
|
|
|
# Config setup
|
|
props = data_config.Properties('./assets/Sim_props/gui_sim_props.yaml') # TODOLOW Parameter?
|
|
props.set_section_stats('sim', fails={}, sim_time={}, spf={}, fin_frame={}, body_collisions={}, self_collisions={})
|
|
props.set_section_stats('render', render_time={})
|
|
|
|
# Force the design to be fitted to mean body shape
|
|
# TODOLOW Support body shape estimation from measurements
|
|
|
|
def_sew_pattern = MetaGarment(
|
|
'Configured_design', self.default_body_params, self.design_params)
|
|
|
|
# Save the pattern
|
|
pattern_folder = self.save(False, save_pattern=def_sew_pattern)
|
|
|
|
# Paths
|
|
paths = PathCofig(
|
|
in_element_path=pattern_folder,
|
|
out_path=self.save_path,
|
|
in_name=def_sew_pattern.name,
|
|
out_name=self.sew_pattern.name + '_3D',
|
|
body_name=self.body_id, # 'f_smpl_average_A40'
|
|
smpl_body=False, # NOTE: depends on chosen body model
|
|
add_timestamp=False
|
|
)
|
|
|
|
# Generate and save garment box mesh (if not existent)
|
|
garment_box_mesh = BoxMesh(paths.in_g_spec, props['sim']['config']['resolution_scale'])
|
|
garment_box_mesh.load()
|
|
garment_box_mesh.serialize(
|
|
paths, store_panels=False, uv_config=props['render']['config']['uv_texture'])
|
|
|
|
# TODOLOW Don't print progress to console with so many lines
|
|
run_sim(
|
|
garment_box_mesh.name,
|
|
props,
|
|
paths,
|
|
save_v_norms=False,
|
|
store_usd=False, # NOTE: False for fast simulation!,
|
|
optimize_storage=False,
|
|
verbose=False
|
|
)
|
|
|
|
# Convert to displayable element
|
|
mesh = trimesh.load_mesh(paths.g_sim)
|
|
|
|
# enable double-sided material for nice viewing
|
|
pbr_material = mesh.visual.material.to_pbr()
|
|
pbr_material.doubleSided = True
|
|
mesh.visual.material = pbr_material
|
|
# export
|
|
mesh.export(paths.g_sim_glb)
|
|
|
|
self.paths_3d = paths
|
|
self.is_in_3D = True
|
|
|
|
return paths.out_el, paths.g_sim_glb.name
|
|
|
|
# Current state
|
|
def is_design_sectioned(self):
|
|
"""Check if design parameters are grouped by sections:
|
|
the top level of design dictionary does not contain actual parameters
|
|
"""
|
|
for param in self.design_params:
|
|
if 'v' in self.design_params[param]:
|
|
return False
|
|
return True
|
|
|
|
def is_slow_design(self) -> bool:
|
|
"""Check is parameters that result in slow pattern generation are enabled
|
|
|
|
E.g. curved armhole evaluation
|
|
"""
|
|
# Pants
|
|
if (self.design_params['meta']['bottom']['v'] == 'Pants'):
|
|
return True
|
|
|
|
# Upper garment
|
|
is_not_upper = self.design_params['meta']['upper']['v'] is None
|
|
if is_not_upper:
|
|
return False
|
|
|
|
# Upper + fitted + strapless
|
|
is_asymm = self.design_params['left']['enable_asym']['v']
|
|
is_fitted = 'Fitted' in self.design_params['meta']['upper']['v']
|
|
is_strapless = self.design_params['fitted_shirt']['strapless']['v']
|
|
is_asymm_strapless = self.design_params['left']['fitted_shirt']['strapless']['v']
|
|
|
|
is_strapless = is_fitted and is_strapless
|
|
is_asymm_strapless = is_fitted and is_asymm_strapless
|
|
|
|
# Has a hoody
|
|
collar_component = self.design_params['collar']['component']['style']['v']
|
|
has_hoody = collar_component is not None and 'Hood' in collar_component
|
|
|
|
# Sleeve potential setup
|
|
sleeves = self.design_params['sleeve']
|
|
is_sleeveless = sleeves['sleeveless']['v']
|
|
is_curve = sleeves['armhole_shape']['v'] == 'ArmholeCurve'
|
|
is_curve = not is_sleeveless and is_curve
|
|
|
|
is_asym_sleeveless = self.design_params['left']['sleeve']['sleeveless']['v']
|
|
is_asymm_curve = self.design_params['left']['sleeve']['armhole_shape']['v'] == 'ArmholeCurve'
|
|
is_asymm_curve = not is_asym_sleeveless and is_asymm_curve
|
|
|
|
if is_asymm:
|
|
right_check = (not is_strapless) and is_curve
|
|
left_check = (not is_asymm_strapless) and is_asymm_curve
|
|
return right_check or left_check
|
|
else:
|
|
return (not is_strapless) and is_curve or has_hoody
|
|
|
|
def save(self, pack=True, save_pattern: Optional[MetaGarment]=None):
|
|
"""Save current garment design to self.save_path """
|
|
|
|
# Save current pattern
|
|
if save_pattern is None:
|
|
save_pattern = self.sew_pattern
|
|
|
|
pattern = save_pattern.assembly()
|
|
|
|
# Save as json file
|
|
self.saved_garment_folder = pattern.serialize(
|
|
self.save_path,
|
|
to_subfolder=True,
|
|
with_3d=False, with_text=False, view_ids=False,
|
|
with_printable=True,
|
|
empty_ok=True
|
|
)
|
|
|
|
self.saved_garment_folder = Path(self.saved_garment_folder)
|
|
self.body_params.save(self.saved_garment_folder)
|
|
|
|
with open(self.saved_garment_folder / 'design_params.yaml', 'w') as f:
|
|
yaml.dump(
|
|
{'design': self.design_params},
|
|
f,
|
|
default_flow_style=False,
|
|
sort_keys=False
|
|
)
|
|
|
|
# pack
|
|
if pack:
|
|
# Only add geometry if design didn't change since last drape
|
|
if not self.is_in_3D:
|
|
self.clear_3d() # Clean any saved 3D if it's not synced with current design
|
|
self.saved_garment_archive = Path(shutil.make_archive(
|
|
self.save_path / '..' / f'{self.saved_garment_folder.name}_{self.id}', 'zip',
|
|
root_dir=self.save_path
|
|
))
|
|
|
|
print(f'Success! {self.sew_pattern.name} saved to {self.saved_garment_folder}')
|
|
|
|
return self.saved_garment_archive if pack else self.saved_garment_folder
|
|
|