import os, sys, time, bpy, math, requests, bmesh, json, shutil from PIL import Image import platform if platform.system() == 'Windows': sys.path.append('e:\\libs\\') #sys.path.append('libs') else: sys.path.append('/data/deploy/make3d/make2/libs/') import config, libs, libs_db,main_service_db def bmesh_copy_from_object(obj, transform=True, triangulate=True, apply_modifiers=False): """Returns a transformed, triangulated copy of the mesh""" assert obj.type == 'MESH' if apply_modifiers and obj.modifiers: import bpy depsgraph = bpy.context.evaluated_depsgraph_get() obj_eval = obj.evaluated_get(depsgraph) me = obj_eval.to_mesh() bm = bmesh.new() bm.from_mesh(me) obj_eval.to_mesh_clear() else: me = obj.data if obj.mode == 'EDIT': bm_orig = bmesh.from_edit_mesh(me) bm = bm_orig.copy() else: bm = bmesh.new() bm.from_mesh(me) if transform: matrix = obj.matrix_world.copy() if not matrix.is_identity: bm.transform(matrix) matrix.translation.zero() if not matrix.is_identity: bm.normal_update() if triangulate: bmesh.ops.triangulate(bm, faces=bm.faces) return bm def find_pid_objname(pid): for obj in bpy.data.objects: if obj.name.startswith(str(pid)): return obj.name def reload_obj(pid): obj_filename = os.path.join(config.workdir, pid, 'output', f'{pid}.obj') bpy.ops.wm.read_homefile() bpy.ops.object.delete(use_global=False, confirm=False) bpy.ops.import_scene.obj(filepath=obj_filename) bpy.context.scene.unit_settings.scale_length = 1 bpy.context.scene.unit_settings.length_unit = 'CENTIMETERS' bpy.context.scene.unit_settings.mass_unit = 'GRAMS' obj = bpy.context.selected_objects[0] bpy.context.view_layer.objects.active = obj obj.select_set(True) def base_fix(pid): print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 开始模型基础校正...') start_time = time.time() # 统一文件名规则 def fix_filename(pid): if os.path.exists(os.path.join(config.workdir, pid, 'output', f'{pid}.jpg')): print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 已经是最新文件名规则,无需处理') return elif os.path.exists(os.path.join(config.workdir, pid, 'output', f'{pid}_u0_v0_diffuse.jpg')): texture_filename_end = '_u0_v0_diffuse.jpg' elif os.path.exists(os.path.join(config.workdir, pid, 'output', f'{pid}_u1_v1.jpg')): texture_filename_end = '_u1_v1.jpg' os.rename(os.path.join(config.workdir, pid, 'output', f'{pid}{texture_filename_end}'), os.path.join(config.workdir, pid, 'output', f'{pid}.jpg')) with open(os.path.join(config.workdir, pid, 'output', f'{pid}.mtl'), 'r') as f: lines = f.readlines() lines = [line.replace(texture_filename_end, '.jpg') for line in lines] with open(os.path.join(config.workdir, pid, 'output', f'{pid}.mtl'), 'w') as f: f.writelines(lines) f.close() fix_filename(pid) # 统一blender环境 reload_obj(pid) # 统一模型方向、位置、大小... pid_objname = find_pid_objname(pid) bpy.data.objects[pid_objname].rotation_euler = (0, 0, 0) bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_VOLUME', center='MEDIAN') bpy.context.object.location[0] = 0 bpy.context.object.location[1] = 0 bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 模型基础校正完成,共费时{libs.diff_time(start_time)}') def export_and_update_obj(pid): print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 开始导出并上传模型...') start_time = time.time() obj_filename = os.path.join(config.workdir, pid, 'output', f'{pid}.obj') bpy.ops.export_scene.obj(filepath=obj_filename) # 上传到oss config.oss_bucket.put_object_from_file(f'objs/auto/{pid}/{pid}.obj', os.path.join(config.workdir, pid, 'output', f'{pid}.obj')) config.oss_bucket.put_object_from_file(f'objs/auto/{pid}/{pid}.mtl', os.path.join(config.workdir, pid, 'output', f'{pid}.mtl')) config.oss_bucket.put_object_from_file(f'objs/auto/{pid}/{pid}.jpg', os.path.join(config.workdir, pid, 'output', f'{pid}.jpg')) config.oss_bucket.put_object_from_file(f'objs/auto/{pid}/{pid}.obj.rcInfo', os.path.join(config.workdir, pid, 'output', f'{pid}.obj.rcInfo')) print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 模型导出并上传完成,共费时{libs.diff_time(start_time)}') def resize_texture_and_reload_obj(pid, ratio=0.5): print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 开始压缩贴图并重载模型...') start_time = time.time() bpy.ops.wm.quit_blender() image_name = os.path.join(config.workdir, pid, 'output', f'{pid}.jpg') img = Image.open(image_name) w, h = img.size img = img.resize((int(w * ratio), int(h * ratio))) img.save(image_name, optimize=True, quality=95) reload_obj(pid) print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 贴图压缩并重载模型完成,共费时{libs.diff_time(start_time)}') def export_and_update_glbs(pid): print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 开始导出并上传审核模型和3D相册模型glb文件...') start_time = time.time() headcount = libs.getHeadCount(pid) pid_objname = find_pid_objname(pid) obj = bpy.data.objects[pid_objname] obj.select_set(True) model_info = {} model_info['headcount'] = headcount model_info['faces'] = round(len(obj.data.polygons) / 10000) model_info['height'] = round(obj.dimensions.y * 100) # bpy.ops.wm.save_as_mainfile(filepath=os.path.join(config.workdir, pid, f'{pid}.blend')) # 统一缩放到9cm标准尺寸 scale = 90 / bpy.data.objects[pid_objname].dimensions.y bpy.data.objects[pid_objname].scale = (scale, scale, scale) bpy.ops.object.transform_apply(scale=True) # bpy.ops.wm.save_as_mainfile(filepath=os.path.join(config.workdir, pid, f'{pid}-9cm.blend')) bm = bmesh_copy_from_object(obj) model_info['volume'] = round(bm.calc_volume(), 2) model_info['weight'] = round(model_info['volume'] * 1.226, 2) print(f'{pid}的模型数据:{model_info}') res = requests.get(f'{config.urls["upload_model_info_url"]}?pid={pid}&headcount={headcount}&faces={model_info["faces"]}&volume={model_info["volume"]}&weight={model_info["weight"]}&height={model_info["height"]}') print('上传模型数据:', res.text) # with open(os.path.join(config.sharedir, 'model_info', f'{pid}.json'), 'w') as f: # json.dump(model_info, f) # f.close() # 先生成审核模型 faces_dest = 300000 * headcount # 减面 faces_current = len(bpy.data.objects[pid_objname].data.polygons) print(f'当前面数:{faces_current},目标面数:{faces_dest}') bpy.ops.object.modifier_add(type='DECIMATE') bpy.context.object.modifiers["Decimate"].ratio = faces_dest / faces_current bpy.ops.object.modifier_apply(modifier="Decimate") glb_filename = os.path.join(config.workdir, pid, 'output', f'{pid}.glb') bpy.ops.export_scene.gltf(filepath=glb_filename, export_format='GLB', export_apply=True, export_jpeg_quality=75, export_draco_mesh_compression_enable=False) # 再生成数字模型 faces_dest = 120000 * headcount # 减面 faces_current = len(bpy.data.objects[pid_objname].data.polygons) print(f'当前面数:{faces_current},目标面数:{faces_dest}') bpy.ops.object.modifier_add(type='DECIMATE') bpy.context.object.modifiers["Decimate"].ratio = faces_dest / faces_current bpy.ops.object.modifier_apply(modifier="Decimate") glb_filename = os.path.join(config.workdir, pid, 'output', f'{pid}-3d.glb') bpy.ops.export_scene.gltf(filepath=glb_filename, export_format='GLB', export_apply=True, export_jpeg_quality=75, export_draco_mesh_compression_enable=False) os.system(f'gltfpack -c -i {os.path.join(config.workdir, pid, "output", f"{pid}.glb")} -o {os.path.join(config.workdir, pid, "output", f"{pid}-pack.glb")}') config.oss_bucket.put_object_from_file(f'glbs/auto/{pid}.glb', os.path.join(config.workdir, pid, 'output', f'{pid}-pack.glb')) config.oss_bucket.put_object_from_file(f'glbs/3d/{pid}.glb', os.path.join(config.workdir, pid, 'output', f'{pid}-3d.glb')) print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} glb文件导出并上传完成,共费时{libs.diff_time(start_time)}') def step3(pid,task_distributed_id=""): print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 开始模型后道处理') start_time = time.time() # 方向、大小、位置等基础校正 base_fix(pid) # 去灰 # 避开人脸白色提纯 # TODO: 人脸五官特征加深 # TODO: 自动UV分割,优先级顺序:脚底、人脸、手指、手臂内侧、大腿内侧、前身、后背、其他 # TODO: 根据UV分割自动修贴图 # 调用blender生成3D相册glb文件和修模审核glb文件,压缩贴图 export_and_update_obj(pid) resize_texture_and_reload_obj(pid) export_and_update_glbs(pid) # 更新本地任务状态,更新云端任务状态 print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 模型后道处理完成,共费时{libs.diff_time(start_time)}') res = requests.post(config.urls['update_status_modelsuccess_url'], data={'id': pid}) print('上传完成更新建模成功状态:', res.text) shutil.rmtree(os.path.join(config.workdir, pid), ignore_errors=True) libs_db.finish_task({"task_type": "make", "task_key": pid}) if task_distributed_id: #因为step2 和step3 是一起跑的,所以这一步先注释掉更新子表的finished_at #main_service_db.update_task_distributed_detail({"task_distributed_id":task_distributed_id,"finished_at":time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}) #更新主表的status 和 finished_at main_service_db.update_task_distributed({"id":task_distributed_id,"status":2,"finished_at":time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}) return def main(pid): if pid == '0': while True: # 取本地mysql队列任务,完成第三步的建模后处理任务 step3(pid) else: step3(pid) if __name__ == '__main__': # 取本地mysql队列任务,完成第三步的建模后处理任务 # 默认循环值守,可传参数运行单一任务,以方便调试 pid = '0' if len(sys.argv) > 1: pids = sys.argv[1].split(',') for pid in pids: main(pid) exit() main(pid)