diff --git a/blender/auto_qrcode.py b/blender/auto_qrcode.py new file mode 100644 index 0000000..811b7bf --- /dev/null +++ b/blender/auto_qrcode.py @@ -0,0 +1,148 @@ +import bpy, sys, os, math, bmesh +# from PIL import Image, ImageDraw, ImageFont + +def gen_qrcode(pid): + fontHeightMax = 40 + fontsize = 1 + qr = qrcode.QRCode() + qr.border = 2 + qr.add_data(pid) + img = qr.make_image(fit=True) + img = img.transform((250, 294), Image.Transform.EXTENT, (0, 0, 250, 294), fillcolor='white') + + cwd = os.path.dirname(os.path.abspath(__file__)) + fontfile = os.path.join(cwd, 'fonts', 'Helvetica.ttf') + font = ImageFont.truetype(fontfile, fontsize) + while font.getsize(pid)[1] <= fontHeightMax and font.getsize(pid)[0] <= 240: + fontsize += 1 + font = ImageFont.truetype(fontfile, fontsize) + fontsize -= 1 + + captionx = (250 - font.getsize(pid)[0]) / 2 + draw = ImageDraw.Draw(img) + draw.text((captionx, 242), pid, font=font) + img.show() + img.save(f'{workdir}{pid}.png') + +def auto_rotate(pid): + # 坐标复位 + obj = bpy.context.selected_objects[0] + obj.rotation_euler[0] = 0 + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_VOLUME', center='MEDIAN') + bpy.ops.object.align(align_mode='OPT_1', relative_to='OPT_1', align_axis={'Y', 'Z'}) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + # bpy.ops.export_scene.obj(filepath=f'{workdir}{pid}_align_yz.obj') + + # 躺平到打印机排版需要的坐标与角度 + obj.rotation_euler = (math.radians(90), math.radians(90), 0) + bpy.ops.object.transform_apply(rotation=True) + # bpy.ops.export_scene.obj(filepath=f'{workdir}{pid}_rotate_y90.obj') + + heights = {} + min_height = 999999 + min_i = 0 + max_height = -999999 + max_i = 0 + + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_VOLUME', center='MEDIAN') + bpy.ops.object.align(align_mode='OPT_1', relative_to='OPT_3', align_axis={'X', 'Y', 'Z'}) + + # 步进精度2旋转X轴到180度,找到Y轴最低点和最高点,其中最低点为打印 + step = 2 + i = 0 + while i <= 180: + obj.rotation_euler = (math.radians(step), 0, 0) + bpy.ops.object.transform_apply(rotation=True) + if obj.dimensions[1] < min_height: + min_height = obj.dimensions[1] + min_i = i + if obj.dimensions[1] > max_height: + max_height = obj.dimensions[1] + max_i = i + heights[i] = (obj.dimensions[0], obj.dimensions[1], obj.dimensions[2]) + print(i, heights[i]) + i += step + + obj.rotation_euler = (0, 0, 0) + bpy.ops.object.transform_apply(rotation=True) + obj.rotation_euler = (math.radians(min_i), 0, 0) + bpy.ops.object.transform_apply(rotation=True) + bpy.ops.export_scene.obj(filepath=f'{workdir}{pid}.obj') + + # obj.rotation_euler = (0, 0, 0) + # bpy.ops.object.transform_apply(rotation=True) + # obj.rotation_euler = (math.radians(max_i), 0, 0) + # bpy.ops.object.transform_apply(rotation=True) + # bpy.ops.export_scene.obj(filepath=f'{workdir}{pid}_maxz.obj') + print(f'最小高度: {min_height} @ {heights[min_i]}min_i:{min_i}' , f'最大高度: {max_height} @ {heights[max_i]}max_i:{max_i}') + +def cut_obj(pid): + # 根据定位用一个面切割模型 + offset = 45.5 + radian = math.radians(90) + bpy.ops.mesh.primitive_plane_add(size=200, enter_editmode=False, align='WORLD', location=(offset, 0, 0), rotation=(0, radian, 0), scale=(1, 1, 1)) + + # 布尔切割,保留交集切面 + bpy.ops.object.modifier_add(type='BOOLEAN') + bpy.context.object.modifiers["Boolean"].object = bpy.data.objects[pid] + bpy.context.object.modifiers["Boolean"].operation = 'INTERSECT' + bpy.context.object.modifiers["Boolean"].solver = 'FAST' + bpy.ops.object.modifier_apply(modifier="Boolean") + + # 拆分切割面为多个多边形,然后遍历多边形,找到最大的面积 + bpy.ops.mesh.separate(type='LOOSE') + + max_area = 0 + max_obj = None + for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.name.startswith('Plane'): + area = obj.data.polygons[0].area + if area > max_area: + max_area = area + max_obj = obj + + # 选中最大面积的多边形,然后计算中心点 + bpy.ops.object.select_all(action='DESELECT') + max_obj.select_set(True) + bpy.context.view_layer.objects.active = max_obj + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') + + return max_obj + +def main(): + filename = f'{workdir}{pid}.obj' + print('正在处理:', filename) + bpy.ops.import_scene.obj(filepath=filename) + + auto_rotate(pid) + # gen_qrcode(pid) + + # 脚底切片,查找最大面积,计算中心点,计算坐标位置,怼入二维码贴片 + max_obj = cut_obj(pid) + bpy.ops.import_scene.obj(filepath=f'{workdir}qr.obj') + qr_obj = bpy.data.objects['Cube'] + shore_obj = bpy.data.objects['Cube.001'] + # bpy.data.objects['Cube'].origin_set(type='ORIGIN_GEOMETRY') + # bpy.data.objects['Cube.001'].origin_set(type='ORIGIN_GEOMETRY') + # bpy.data.objects['Cube.002'].origin_set(type='ORIGIN_GEOMETRY') + # bpy.data.objects['Cube.003'].origin_set(type='ORIGIN_GEOMETRY') + bpy.data.objects['Cube'] = (math.radians(90), math.radians(90), 0) + bpy.data.objects['Cube.001'].rotation_euler = (math.radians(90), math.radians(90), 0) + bpy.data.objects['Cube.002'].rotation_euler = (math.radians(90), math.radians(90), 0) + bpy.data.objects['Cube.003'].rotation_euler = (math.radians(90), math.radians(90), 0) + qr_obj.location = (max_obj.location[0] - qr_obj.dimensions[1] / 2 - shore_obj.dimensions[1]/2, max_obj.location[1], max_obj.location[2]) + shore_obj.location = (qr_obj.location[0] - shore_obj.dimensions[1]/2, max_obj.location[1], max_obj.location[2]) + bpy.data.objects['Cube.002'].location = (shore_obj.location[0], shore_obj.location[1]+0.2, shore_obj.location[2]) + bpy.data.objects['Cube.003'].location = (shore_obj.location[0], shore_obj.location[1]-0.2, shore_obj.location[2]) + + bpy.ops.object.transform_apply(rotation=True, location=True, scale=True) + + +if __name__ == '__main__': + workdir = '/home/water/Downloads/' + if len(sys.argv) - (sys.argv.index("--") +1) < 1: + print("Usage: blender -b -P auto_qrcode.py -- ") + sys.exit(1) + pid = sys.argv[sys.argv.index("--") + 1] + main() \ No newline at end of file diff --git a/blender/autofix.py b/blender/autofix.py new file mode 100644 index 0000000..7717f68 --- /dev/null +++ b/blender/autofix.py @@ -0,0 +1,160 @@ +from math import radians +import sys, os, time, bpy, requests, json, bmesh +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import platform +if platform.system() == 'Windows': + sys.path.append('e:\\libs\\') +else: + sys.path.append('/data/deploy/make3d/make2/libs/') +import config + +def get_obj_version(filename): + with open(filename, 'r') as f: + for line in f: + if line.startswith('# Engine version'): + return float(line.split(' ')[-1][1:].strip()[:3]) + exit(0) + return None + +def delete_lines_in_file(filename, count): + with open(filename, 'r') as f: + lines = f.readlines() + lines = lines[count:] + with open(filename, 'w') as f: + f.writelines(lines) + +def diff_minutes_and_seconds(start): + hours = int((time.time() - start) / 3600) + minutes = int((time.time() - start) / 60) + seconds = int((time.time() - start) % 60) + microseconds = int(int((time.time() - start) * 1000000) % 1000000 / 1000) + return f'{hours}:{minutes}:{seconds}.{microseconds}' + +def get_headcount(pid): + res = requests.get(config.urls['get_printinfo_url'], params={'id': pid}) + print('get_printsize_url:', res.url) + print('res:', res.text) + if res.status_code != 200: + print('获取人数失败,程序退出') + exit(1) + res = json.loads(res.text) + return res['data']['headcount'] + +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 getPSid(pid): + get_psid_url = 'https://mp.api.suwa3d.com/api/customerP3dLog/photoStudio' + res = requests.get(get_psid_url, params={'pid': pid}) + res = json.loads(res.text) + return str(res['data']) + +def getPSRotation(pid): + get_ps_rotation_url = 'https://mp.api.suwa3d.com/api/takephotoOrder/angle' + res = requests.get(get_ps_rotation_url, params={'pid': pid}) + res = json.loads(res.text) + rotation = (radians(0), radians(0), radians(int(res['data']))) + return rotation + +def main(): + start = time.time() + workdir = 'd:\\' + + if len(sys.argv) - (sys.argv.index("--") +1) < 1: + print("Usage: blender -b -P autofix.py -- ") + sys.exit(1) + input_file = sys.argv[sys.argv.index("--") + 1] + + for pid in input_file.split(','): + psid = getPSid(pid) + + bpy.ops.wm.read_homefile() + # bpy.context.scene.unit_settings.scale_length = 0.001 + bpy.context.scene.unit_settings.length_unit = 'CENTIMETERS' + bpy.context.scene.unit_settings.mass_unit = 'GRAMS' + bpy.ops.object.delete(use_global=False, confirm=False) + + filename = f'{workdir}{pid}\\output\{pid}.obj' + print('正在处理:', filename) + bpy.ops.import_scene.obj(filepath=filename) + bpy.ops.object.align(align_mode='OPT_1', relative_to='OPT_2', align_axis={'Z'}) + print('import obj time:', diff_minutes_and_seconds(start)) + + # rotate obj + obj = bpy.context.selected_objects[0] + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + rotation = getPSRotation(pid) + print('rotation:', rotation) + obj.rotation_euler = rotation + print('rotate obj time:', diff_minutes_and_seconds(start)) + # resize object + scale = 90 / obj.dimensions.z + obj.scale = (scale, scale, scale) + 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) + # bpy.ops.wm.save_as_mainfile(filepath=f'{workdir}{pid}\\output\{pid}_4.blend') + + bm = bmesh_copy_from_object(obj) + obj_volume = round(bm.calc_volume() / 1000, 3) + print('volume:', obj_volume) + print('weight:', obj_volume * 1.2, 'g') + + faces = len(obj.data.polygons) + print('faces:', faces) + + # save object + bpy.ops.export_scene.obj(filepath=f'{workdir}{pid}\\output\{pid}.obj') + + # 生成数字模型 + headcount = get_headcount(pid) + faces_dest = 120000 * headcount + + # 减面 + faces_current = len(obj.data.polygons) + bpy.ops.object.modifier_add(type='DECIMATE') + bpy.context.object.modifiers["Decimate"].ratio = faces_dest / faces_current + bpy.ops.object.modifier_apply(modifier="Decimate") + + bpy.ops.export_scene.gltf(filepath=os.path.join(workdir, pid, 'output', f'{pid}_decimate.glb'), export_format='GLB', export_apply=True, export_jpeg_quality=75, export_draco_mesh_compression_enable=False) + + config.oss_bucket.put_object_from_file(f'glbs/3d/{pid}.glb', os.path.join(workdir, pid, 'output', f'{pid}_decimate.glb')) + print('免费体验3d相册已生成,上传glb文件:', f'glbs/3d/{pid}.glb 完成') + # render scene to a file + # bpy.context.scene.render.filepath = f'{workdir}{pid}_fixed.png' + # bpy.ops.render.render(write_still=True, use_viewport=True) + print('render time:', diff_minutes_and_seconds(start)) + + bpy.ops.wm.quit_blender() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/blender/autofix10.py b/blender/autofix10.py new file mode 100644 index 0000000..fa3b44a --- /dev/null +++ b/blender/autofix10.py @@ -0,0 +1,144 @@ +from math import radians +import sys, os, time, bpy, requests, json, bmesh + +def get_obj_version(filename): + with open(filename, 'r') as f: + for line in f: + if line.startswith('# Engine version'): + return float(line.split(' ')[-1][1:].strip()[:3]) + exit(0) + return None + +def delete_lines_in_file(filename, count): + with open(filename, 'r') as f: + lines = f.readlines() + lines = lines[count:] + with open(filename, 'w') as f: + f.writelines(lines) + +def diff_minutes_and_seconds(start): + hours = int((time.time() - start) / 3600) + minutes = int((time.time() - start) / 60) + seconds = int((time.time() - start) % 60) + microseconds = int(int((time.time() - start) * 1000000) % 1000000 / 1000) + return f'{hours}:{minutes}:{seconds}.{microseconds}' + +def getPSid(pid): + get_psid_url = 'https://mp.api.suwa3d.com/api/customerP3dLog/photoStudio' + res = requests.get(get_psid_url, params={'pid': pid}) + res = json.loads(res.text) + return str(res['data']) + +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 main(): + start = time.time() + config = { + '0': { + 'rotation': (radians(0), radians(0), radians(0)), + }, + '1': { + 'rotation': (radians(0), radians(0), radians(66)), + }, + '29': { + 'rotation': (radians(0), radians(0), radians(180)), + }, + '45': { + 'rotation': (radians(0), radians(0), radians(105)), + }, + '46': { + 'rotation': (radians(0), radians(0), radians(-10)), + }, + '74': { + 'rotation': (radians(0), radians(0), radians(110)), + }, + '75': { + 'rotation': (radians(0), radians(0), radians(210)), + }, + } + workdir = 'd:\\' + + if len(sys.argv) - (sys.argv.index("--") +1) < 1: + print("Usage: blender -b -P autofix.py -- ") + sys.exit(1) + input_file = sys.argv[sys.argv.index("--") + 1] + + for pid in input_file.split(','): + psid = getPSid(pid) + + bpy.ops.object.delete(use_global=False, confirm=False) + + filename = f'{workdir}{pid}\\output\{pid}.obj' + print('正在处理:', filename) + bpy.ops.import_scene.obj(filepath=filename) + bpy.ops.object.align(relative_to='OPT_1', align_axis={'X'}) + # bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + print('import obj time:', diff_minutes_and_seconds(start)) + + # rotate obj + obj = bpy.context.selected_objects[0] + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + if psid in config: + obj.rotation_euler = config[psid]['rotation'] + else: + obj.rotation_euler = config['0']['rotation'] + print('rotate obj time:', diff_minutes_and_seconds(start)) + # bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + # resize object + obj_scale = 90 / obj.dimensions.z + print('scale:', obj_scale) + + obj.scale = (obj_scale, obj_scale, obj_scale) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + bm = bmesh_copy_from_object(obj) + obj_volume = round(bm.calc_volume() / 1000, 3) + print('volume:', obj_volume) + print('weight:', obj_volume * 1.2, 'g') + + faces = len(obj.data.polygons) + print('faces:', faces) + + # save object + bpy.ops.export_scene.obj(filepath=f'{workdir}{pid}\\output\{pid}.obj') + # render scene to a file + # bpy.context.scene.render.filepath = f'{workdir}{pid}_fixed.png' + # bpy.ops.render.render(write_still=True, use_viewport=True) + print('render time:', diff_minutes_and_seconds(start)) + + bpy.ops.wm.quit_blender() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/blender/cal_foot_position.py b/blender/cal_foot_position.py new file mode 100644 index 0000000..9a32854 --- /dev/null +++ b/blender/cal_foot_position.py @@ -0,0 +1,628 @@ +import os, sys, bpy, math, time, platform, cairosvg, ppf.datamatrix, shutil, requests, json, redis, oss2, heapq +import matplotlib.pyplot as plt +from PIL import Image +import numpy as np +from addon_utils import enable +enable('io_import_images_as_planes') +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import config + +def gen_data_matrix(print_id, qr_path, size = 300): + svg = ppf.datamatrix.DataMatrix(f'p{print_id}').svg() + cairosvg.svg2png(bytestring=svg, write_to=qr_path, output_width=size, output_height=size, background_color='white') + +def active_object(obj): + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + +def down_obj_fromoss(pid, print_type=1, order_id=None): + # print_type:// 打印状态 1:正常打印 2:重打 3:加打,4: 样品 + print('开始下载obj文件...' , pid) + + if not order_id is None: + path = os.path.join(workdir, f'{pid}_{order_id}') + else: + path = os.path.join(workdir, pid) + if not os.path.exists(path): os.makedirs(path) + + # 根据前缀获取文件列表 + prefix = f'objs/print/{pid}/' + filelist = oss2.ObjectIteratorV2(config.oss_bucket, prefix=prefix) + find = False + for file in filelist: + filename = file.key.split('/')[-1] + if filename == '': continue + if filename.endswith(f'{pid}.obj'): + find = True + localfile = os.path.join(path, filename) + res = config.oss_bucket.get_object_to_file(file.key, localfile) + print(f'下载文件:{file.key},状态:{res.status}') + + if not find: + for file in os.listdir(path): + if file.endswith('.obj'): + print('找到其他obj文件,采用这个文件来生成需要的尺寸', file) + shutil.copy(os.path.join(path, file), os.path.join(path, f'{pid}.obj')) + find = True + break + if not find: + print('找不到obj文件,异常退出') + sys.exit(1) + +def find_obj(pid, order_id): + find = False + if not os.path.exists(os.path.join(workdir, f'{pid}_{order_id}', f'{pid}.mtl')): + print('没有找到obj模型文件,开始下载') + down_obj_fromoss(pid, order_id=order_id) + if os.path.exists(os.path.join(workdir, f'{pid}_{order_id}', f'{pid}.jpg')): + shutil.move(os.path.join(workdir, f'{pid}_{order_id}', f'{pid}.jpg'), os.path.join(workdir, f'{pid}_{order_id}', f'{pid}Tex1.jpg')) + with open(os.path.join(workdir, f'{pid}_{order_id}', f'{pid}.mtl'), 'r') as f: + lines = f.readlines() + lines = [line.replace(f'map_Kd {pid}.jpg', f'map_Kd {pid}Tex1.jpg') for line in lines] + with open(os.path.join(workdir, f'{pid}_{order_id}', f'{pid}.mtl'), 'w') as f: + f.writelines(lines) + + filelist = os.listdir(os.path.join(workdir, f'{pid}_{order_id}')) + for filename in filelist: + if '9cm' in filename: + find = True + return filename + for filename in filelist: + if f'{pid}.obj' in filename: + find = True + return filename + for filename in filelist: + if '.obj' in filename: + find = True + return filename + print('没有找到obj模型文件') + return '' + +def find_pid_objname(pid): + for obj in bpy.data.objects: + if obj.name.startswith(str(pid)): + return obj.name + +def get_obj_max_foot(): + filename = find_obj(pid, order_id) + + filename = os.path.join(workdir, f'{pid}_{order_id}', filename) + bpy.ops.wm.read_homefile() + bpy.context.preferences.view.language = 'en_US' + bpy.ops.object.delete(use_global=False, confirm=False) + bpy.ops.import_scene.obj(filepath=filename) + bpy.context.scene.unit_settings.scale_length = 0.001 + 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) + + pid_objname = find_pid_objname(pid) + + scale = 90 / obj.dimensions.y + obj.scale = (scale, scale, scale) + + bpy.ops.object.align(align_mode='OPT_1', relative_to='OPT_1', align_axis={'Z'}) + + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') + obj.location[0] = 0 + obj.location[1] = 0 + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + if pid in ('76461', '98871', '112139'): + bpy.ops.mesh.primitive_plane_add(size=200, enter_editmode=False, align='WORLD', location=(0, 0, 0.6), scale=(1, 1, 1)) + else: + bpy.ops.mesh.primitive_plane_add(size=200, enter_editmode=False, align='WORLD', location=(0, 0, 0.2), scale=(1, 1, 1)) + # bpy.ops.wm.save_as_mainfile(filepath=os.path.join(workdir, f'{pid}_{order_id}', f'{pid}_{order_id}.blend')) + bpy.ops.object.modifier_add(type='BOOLEAN') + bpy.context.object.modifiers["Boolean"].object = bpy.data.objects[pid_objname] + bpy.context.object.modifiers["Boolean"].operation = 'INTERSECT' + bpy.context.object.modifiers["Boolean"].solver = 'FAST' + bpy.ops.object.modifier_apply(modifier="Boolean") + + bpy.ops.mesh.separate(type='LOOSE') + + max_area = 0 + for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.name.startswith('Plane'): + if len(obj.data.polygons) == 0: continue + area = obj.data.polygons[0].area + if area > max_area: + max_area = area + obj.name = 'foot' + print(f'最大脚底板面积: {max_area} cm²') + if max_area < 5: + print('最大脚底板面积太小,脚底模型可能有破损,异常退出') + sys.exit(1) + # bpy.ops.wm.save_as_mainfile(filepath=os.path.join(workdir, f'{pid}_{order_id}', f'{pid}_{order_id}.blend')) + active_object(bpy.data.objects['foot']) + foot_points = get_plane_points(bpy.data.objects['foot']) + # plot(get_plane_points(bpy.data.objects['foot']), 'blue') + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') + + # print(f"location: {bpy.data.objects['foot'].location}") + bpy.ops.import_image.to_plane(files=[{"name":"qr.png"}], directory=f"{workdir}{pid}_{order_id}", relative=False) + # bpy.ops.mesh.primitive_plane_add(size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) + + # print(f"new_location: {bpy.data.objects['foot'].location}") + + active_object(bpy.data.objects['qr']) + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') + bpy.data.objects['qr'].rotation_euler[0] = 0 + bpy.data.objects['qr'].location = bpy.data.objects['foot'].location + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + # bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') + # print(f"qr_location: {bpy.data.objects['qr'].location}") + # print(f'qr_points: {get_plane_points(bpy.data.objects["qr"])}') + # plot(get_plane_points(bpy.data.objects['qr']), 'red') + return foot_points + +def euclidean_distance(p1, p2): + return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5 + +def nearest_neighbor_sort(points): + print('nearest neighbor sort') + n = len(points) + visited = set() + sorted_points = [] + i = 1 + # Start from the first point + current_point = points[0] + + #while len(visited) < n: + while i < n: + i += 1 + sorted_points.append(current_point) + visited.add(current_point) + + # Create a priority queue to store distances and points + distance_queue = [] + for point in points: + if point not in visited: + distance = euclidean_distance(current_point, point) + heapq.heappush(distance_queue, (distance, point)) + + # Find the nearest unvisited point + while distance_queue: + distance, next_point = heapq.heappop(distance_queue) + if next_point not in visited: + current_point = next_point + break + + + return sorted_points + +def get_max_qr(foot_points): + + def dis_flag(square, foot_points): + for point in foot_points: + dis0 = get_distance_from_point_to_line(point, square[0], square[1]) + dis1 = get_distance_from_point_to_line(point, square[1], square[2]) + dis2 = get_distance_from_point_to_line(point, square[2], square[3]) + dis3 = get_distance_from_point_to_line(point, square[3], square[0]) + min_dis = min([dis0, dis1, dis2, dis3]) + return min_dis > 0.5 + + def get_distance_from_point_to_line(point, line_point1, line_point2): + # 对于两点坐标为同一点时,返回点与点的距离 + if line_point1 == line_point2: + point_array = np.array(point) + point1_array = np.array(line_point1) + return np.linalg.norm(point_array - point1_array) + # 计算直线的三个参数 + A = line_point2[1] - line_point1[1] + B = line_point1[0] - line_point2[0] + C = (line_point1[1] - line_point2[1]) * line_point1[0] + \ + (line_point2[0] - line_point1[0]) * line_point1[1] + # 根据点到直线的距离公式计算距离 + distance = np.abs(A * point[0] + B * point[1] + C) / (np.sqrt(A ** 2 + B ** 2)) + return distance + + # 判断方形是否在轮廓内 + def square_in_polygon_default(square, polygon): + for point in square: + if not point_in_polygon(point, polygon): + return False + return True + + # 自定义二维码初始坐标 + def get_default_qr_points(foot_points): + max_x = max([x[0] for x in foot_points]) + min_x = min([x[0] for x in foot_points]) + max_y = max([x[1] for x in foot_points]) + min_y = min([x[1] for x in foot_points]) + + center_x, center_y = (max_x + min_x) / 2, (max_y + min_y) / 2 + flag_default = point_in_polygon((center_x, center_y), foot_points) + if not flag_default: + index_move = 0 + while not flag_default and index_move < 5: + center_x = (center_x + min_x) / 2 + index_move += 1 + flag_default = point_in_polygon((center_x, center_y), foot_points) + if not flag_default: + while not flag_default: + center_y = (center_y + min_y) / 2 + flag_default = point_in_polygon((center_x, center_y), foot_points) + + length = min((center_x - min_x) / 2, (center_y - min_y) / 2) / 2 + # 在不规则平面中心位置初始化一个方形 + qr_points = [(center_x - length, center_y + length), (center_x + length, center_y + length), (center_x + length, center_y - length), (center_x - length, center_y - length)] + qr_points = scale_qr_new(foot_points, qr_points, length, (center_x, center_y), scale=1.05) + return qr_points + + def scale_qr_new(foot_points, qr_points, length, center, scale=1.1): + default_flag = flag = square_in_polygon(qr_points, foot_points) + center_x, center_y = center[0], center[1] + if flag: + while default_flag == flag: + length *= scale + # 对每个点进行放大操作并更新坐标 + qr_points = [((x - center_x) * scale + center_x, (y - center_y) * scale + center_y) for x, y in qr_points] + flag = square_in_polygon_default(qr_points, foot_points) and square_in_polygon(qr_points, foot_points) + else: + while default_flag == flag: + length /= scale + # 对每个点进行缩小操作并更新坐标 + qr_points = [((x - center_x) / scale + center_x, (y - center_y) / scale + center_y) for x, y in qr_points] + flag = square_in_polygon_default(qr_points, foot_points) and square_in_polygon(qr_points, foot_points) + return qr_points + + # 获取旋转后方形 根据方形原坐标旋转 + def cal_rota_points(qr_points, center, angle): + center_x, center_y = center[0], center[1] + if angle > 0: + qr_points_after_rotate = [] + for point in qr_points: + new_x = (point[0] - center_x) * math.cos(angle) - (point[1] - center_y) * math.sin(angle) + center_x + new_y = (point[0] - center_x) * math.sin(angle) + (point[1] - center_y) * math.cos(angle) + center_y + qr_points_after_rotate.append((new_x, new_y)) + return qr_points_after_rotate + else: + return qr_points + + # 取中点 + def cal_middle_point(p1, p2): + x1, y1 = p1 + x2, y2 = p2 + # 中点 + a1 = (x1 + x2) / 2 + b1 = (y1 + y2) / 2 + return a1, b1 + + def make_points(qr_points): + new_points = [] + index = [0, 1, 2, 3, 0] + for i in range(4): + a, b = cal_middle_point(qr_points[index[i]], qr_points[index[i + 1]]) + new_points.append((a, b)) + new_points.append((cal_middle_point(qr_points[index[i]], (a, b)))) + new_points.append((cal_middle_point(qr_points[index[i + 1]], (a, b)))) + return new_points + + #qr_points = get_default_qr_points(foot_points) + + min_qr_length = 0.5 + + minx = min([p[0] for p in foot_points]) + min_qr_length + maxx = max([p[0] for p in foot_points]) - min_qr_length + miny = min([p[1] for p in foot_points]) + min_qr_length + maxy = max([p[1] for p in foot_points]) - min_qr_length + + def rotate_qr_v3(foot_points, qr_points, scale, angle=1): + best_length = length = cal_square_length(qr_points) + best_angle, default_angle = 0, 0 + center_x, center_y = calculate_center(qr_points) + best_qr_points = qr_points + # 循环1 求最佳angle 不断增大angle角度 + while default_angle <= 90: + qr_points_after_rotate = cal_rota_points(qr_points, (center_x, center_y), default_angle) + # 在当前angle下增加边长 + while square_in_polygon(qr_points_after_rotate, foot_points) and dis_flag(qr_points_after_rotate, foot_points): + flag = True + best_qr_points = qr_points_after_rotate + best_angle = default_angle + best_length = length + # 对每个点进行放大(或缩小)操作并更新坐标 + qr_points = [((x - center_x) * scale + center_x, (y - center_y) * scale + center_y) for x, y in qr_points] + length *= scale + qr_points_after_rotate = cal_rota_points(qr_points, (center_x, center_y), default_angle) + # 限制最大边长 + if best_length > 5: + return best_qr_points, best_angle, best_length + + default_angle += angle + return best_qr_points, best_angle, best_length + + if maxx - minx < maxy - miny: + step = (maxx - minx) / 15 + else: + step = (maxy - miny) / 15 + + x, y = minx, miny + locations = [] + + while x <= maxx: + while y <= maxy: + locations.append((x, y)) + y += step + x += step + y = miny + + # print(f'locations: {locations}') + locations = [point for point in locations if all(cal_distance(point, f) >= min_qr_length for f in foot_points)] + location = locations[0] + qr_points = [(location[0] - 0.5, location[1] - 0.5), (location[0] + 0.5, location[1] - 0.5), (location[0] + 0.5, location[1] + 0.5), (location[0] - 0.5, location[1] + 0.5)] + plot(foot_points) + plot(qr_points, 'yellow') + plt.savefig(f'{workdir}{pid}_{order_id}/fig.png') + + best_qr, max_qr_length, best_location, best_rotation = None, 0, None, 0 + for location in locations: + plt.plot(location[0], location[1], 'ro') + qr_points = move_square(qr_points, location) + if not square_in_polygon(qr_points, foot_points) or not square_in_polygon_default(qr_points, foot_points): + continue + else: + # qr_points = scale_qr(foot_points, qr_points, 1.1) + # qrs.append(qr_points) + rotate_qr, rotate_angle, qr_length = rotate_qr_v3(foot_points, qr_points, 1.1, 1) + if qr_length > max_qr_length: + max_qr_length = qr_length + best_location = location + best_rotation = rotate_angle + best_qr = rotate_qr + + rd = max_qr_length / 1.1 / 2 + x, y = best_location[0], best_location[1] + new_qr_points = [(x - rd, y + rd), (x + rd, y + rd), (x + rd, y - rd), (x - rd, y - rd)] + new_qr_points = cal_rota_points(new_qr_points, best_location, best_rotation) + return new_qr_points, best_location, max_qr_length / 1.1, best_rotation + +def get_plane_points(plane, print_points = False): + points = [] + for edge in plane.data.edges: + point_index = edge.vertices[0] + point3d = plane.data.vertices[point_index].co + if print_points: print(point3d) + points.append((point3d[0], point3d[1])) + return points + +def point_in_polygon(point, polygon): + num_intersections = 0 + for i in range(len(polygon)): + p1, p2 = polygon[i], polygon[(i + 1) % len(polygon)] + if (p1[1] > point[1]) != (p2[1] > point[1]): + if point[0] < (p2[0] - p1[0]) * (point[1] - p1[1]) / (p2[1] - p1[1]) + p1[0]: + num_intersections += 1 + return num_intersections % 2 == 1 + +def square_iou_polygon(square, polygon): + for point in square: + if point_in_polygon(point, polygon): + return True + return False + +def square_in_polygon(square, polygon): + for point in polygon: + if point_in_polygon(point, square): + return False + return True + +def plot(points, color='blue'): + x = [point[0] for point in points] + y = [point[1] for point in points] + if points[-1] != points[0]: + x.append(points[0][0]) + y.append(points[0][1]) + plt.plot(x, y, color=color) + +def scale_qr(foot_points, qr_points, scale = 1.1): + while True: + old_points = qr_points + # 计算正方形的中心坐标 + center_x = sum(x for x, y in qr_points) / len(qr_points) + center_y = sum(y for x, y in qr_points) / len(qr_points) + + # 对每个点进行放大(或缩小)操作并更新坐标 + qr_points = [((x - center_x) * scale + center_x, (y - center_y) * scale + center_y) for x, y in qr_points] + + if not square_in_polygon(qr_points, foot_points): + qr_points = old_points + break + return qr_points + +def rotate_qr(foot_points, qr_points, angle = 0.1): + while True: + old_points = qr_points + # 计算正方形的中心坐标 + center_x = sum(x for x, y in qr_points) / len(qr_points) + center_y = sum(y for x, y in qr_points) / len(qr_points) + + # 对每个点进行放大(或缩小)操作并更新坐标 + qr_points = [(x - center_x, y - center_y) for x, y in qr_points] + + qr_points = [(x * math.cos(angle) - y * math.sin(angle), x * math.sin(angle) + y * math.cos(angle)) for x, y in qr_points] + + qr_points = [(x + center_x, y + center_y) for x, y in qr_points] + + if not square_in_polygon(qr_points, foot_points): + qr_points = old_points + break + return qr_points + +def scale_square(scale, foot_points, back = 0.0): + while True: + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') + old_dimensions = bpy.data.objects['qr'].dimensions.copy() + active_object(bpy.data.objects['qr']) + bpy.data.objects['qr'].scale = (scale, scale, 1) + max_square = get_plane_points(bpy.data.objects['qr']) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + if not square_in_polygon(max_square, foot_points): + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') + bpy.data.objects['qr'].dimensions = (old_dimensions[0] - back, old_dimensions[1] - back, 0) + max_square = get_plane_points(bpy.data.objects['qr']) + location, size = get_square_center_size() + break + return max_square, location, size + +def zoom_square(foot_points, qr_points, center, step_length=0.1): + while True: + old_dimensions = bpy.data.objects['qr'].dimensions.copy() + active_object(bpy.data.objects['qr']) + # print(f'old_dimensions: {old_dimensions}') + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') + bpy.data.objects['qr'].dimensions = (old_dimensions[0] + step_length, old_dimensions[1] + step_length, 0) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') + # print(f'new_dimensions: {bpy.data.objects["qr"].dimensions}') + max_square = get_plane_points(bpy.data.objects['qr']) + if not square_in_polygon(max_square, foot_points): + bpy.data.objects['qr'].dimensions = (old_dimensions[0], old_dimensions[1], 0) + max_square = get_plane_points(bpy.data.objects['qr']) + location, size, length = get_square_center_size() + break + return max_square, location, size, length + +def get_square_center_size(): + active_object(bpy.data.objects['qr']) + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') + location = bpy.data.objects['qr'].location + size = bpy.data.objects['qr'].dimensions + length = size[0] + return location, size, length + +def min_x(plane): + return min([p[0] for p in plane]) +def min_y(plane): + return min([p[1] for p in plane]) +def max_x(plane): + return max([p[0] for p in plane]) +def max_y(plane): + return max([p[1] for p in plane]) + +def cal_square_length(square): + return abs(square[0][0] - square[1][0]) + +def cal_square_area(square): + return abs(square[0][0] - square[1][0]) * abs(square[0][1] - square[3][1]) + +def cal_distance(point1, point2): + return math.sqrt((point1[0] - point2[0])**2 + (point1[1] - point2[1])**2) + +def calculate_center(vertices): + x_sum = sum(x for x, y in vertices) + y_sum = sum(y for x, y in vertices) + center_x = x_sum / len(vertices) + center_y = y_sum / len(vertices) + return center_x, center_y + +def move_square(vertices, new_center): + center_x, center_y = calculate_center(vertices) + # print(f'center_x: {center_x}, center_y: {center_y}') + # print(f'new_center: {new_center}') + x_diff = center_x - new_center[0] + y_diff = center_y - new_center[1] + # print(f'x_diff: {x_diff}, y_diff: {y_diff}') + # print(f'vertices: {vertices}') + new_vertices = [(x - x_diff, y - y_diff) for x, y in vertices] + # print(f'new_vertices: {new_vertices}') + return new_vertices + +def main(workdir, pid, order_id, print_id): + if not os.path.exists(os.path.join(workdir, f'{pid}_{order_id}')): + os.makedirs(os.path.join(workdir, f'{pid}_{order_id}')) + qr_path = os.path.join(workdir, f'{pid}_{order_id}' ,'qr.png') + gen_data_matrix(print_id, qr_path) + + get_obj_max_foot() + + qr_points = get_plane_points(bpy.data.objects['qr']) + active_object(bpy.data.objects['foot']) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + foot_points = get_plane_points(bpy.data.objects['foot']) + # print('foot_points:', foot_points) + foot_points = nearest_neighbor_sort(foot_points) + max_qr, qr_location, max_qr_length, rotation = get_max_qr(foot_points) + + print(f'qr_location: {qr_location}') + plt.plot(qr_location[0], qr_location[1], 'black') + plot(max_qr, 'green') + plt.axis('equal') + plt.savefig(os.path.join(workdir, f'{pid}_{order_id}', 'fig.png')) + + bpy.ops.wm.save_as_mainfile(filepath=f'{workdir}{pid}_{order_id}/{pid}_qr_start.blend') + + qr_position = {} + qr_location = (qr_location[0], qr_location[1], 0) + qr_dimensions = (max_qr_length, max_qr_length, 0) + # print(f'qr_location: {qr_location}') + # print(f'qr_dimensions: {qr_dimensions}') + qr_position["location"] = qr_location + qr_position["dimensions"] = qr_dimensions + qr_position["rotation"] = rotation + print(f'qr_position: {qr_position}') + # with open(os.path.join(workdir, f'{pid}_{order_id}', 'qr_position.txt'), 'w') as f: + # f.write(json.dumps(qr_position)) + + res = requests.get(f'{upload_qr_position_url}?print_id={print_id}&position_data={json.dumps(qr_position)}') + print(f'update_qr_position_url {upload_qr_position_url}:{res.text}') + + bpy.ops.object.load_reference_image(filepath=os.path.join(workdir, f'{pid}_{order_id}', 'qr.png')) + bpy.context.object.rotation_euler = (math.radians(-180), math.radians(0), rotation) + bpy.ops.transform.translate(value=qr_location, orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False, snap=False, snap_elements={'INCREMENT'}, use_snap_project=False, snap_target='CLOSEST', use_snap_self=True, use_snap_edit=True, use_snap_nonedit=True, use_snap_selectable=False, release_confirm=True) + + bpy.context.object.empty_display_size = qr_dimensions[0] + + # for obj in bpy.data.objects: + # if obj.type == 'MESH' and obj.name != pid: + # bpy.data.objects.remove(obj) + + # qr_path = os.path.join(workdir,f'{pid}_{order_id}', f"{pid}_{order_id}Tex1_qr.png") + # jpg_path = os.path.join(workdir,f'{pid}_{order_id}', f"{pid}Tex1.jpg") + # jpg_img = Image.open(jpg_path) + # shutil.copyfile(jpg_path, os.path.join(workdir,f'{pid}_{order_id}', f"{pid}Tex1_noqr.jpg")) + + # bpy.context.scene.eyek.res_x = jpg_img.width + # bpy.context.scene.eyek.res_y = jpg_img.height + # bpy.context.scene.eyek.path_export_image = qr_path + # bpy.data.objects[f'{pid}'].select_set(True) + # bpy.data.objects['Empty'].select_set(True) + # bpy.context.view_layer.objects.active = bpy.data.objects[f'{pid}'] + # bpy.ops.eyek.exe() + + # qr_img = Image.open(qr_path) + # jpg_img.paste(qr_img, (0, 0), qr_img) + # jpg_img.save(jpg_path) + + # plt.axis('equal') + # plt.show() + + # 保存blend文件 + bpy.ops.wm.save_as_mainfile(filepath=f'{workdir}{pid}_{order_id}/{pid}_qr_end.blend') + bpy.ops.wm.quit_blender() + + +if __name__ == '__main__': + get_qr_position_url = 'https://mp.api.suwa3d.com/api/printOrder/getFootCodePositionData' + upload_qr_position_url = 'https://mp.api.suwa3d.com/api/printOrder/updateFootCodeStatus' + get_pid_by_printid_url = 'https://mp.api.suwa3d.com/api/printOrder/getPidByPrintId' + # get_qr_position_url = 'http://172.31.1.254:8199/api/printOrder/getFootCodePositionData' + # upload_qr_position_url = 'http://172.31.1.254:8199/api/printOrder/updateFootCodeStatus' + # get_pid_by_printid_url = 'http://172.31.1.254:8199/api/printOrder/getPidByPrintId' + + if platform.system() == 'Windows': + workdir = 'd:\\print\\' + else: + workdir = '/data/datasets/foot/' + + print(sys.argv) + if len(sys.argv) - (sys.argv.index("--") + 1) < 1: + print("Usage: blender -b -P auto_dm.py -- ") + sys.exit(1) + pid, order_id, print_id = sys.argv[sys.argv.index("--") + 1].split('_') + + main(workdir, pid, order_id, print_id) \ No newline at end of file diff --git a/blender/controlpoints0.dat b/blender/controlpoints0.dat new file mode 100644 index 0000000..aed7d24 Binary files /dev/null and b/blender/controlpoints0.dat differ diff --git a/blender/controlpoints_80.dat b/blender/controlpoints_80.dat new file mode 100644 index 0000000..adf0c4b Binary files /dev/null and b/blender/controlpoints_80.dat differ diff --git a/blender/debug.Text.py b/blender/debug.Text.py new file mode 100644 index 0000000..c208b49 --- /dev/null +++ b/blender/debug.Text.py @@ -0,0 +1,91 @@ +import bpy, sys, os, math + +pid = '26385' +workdir = '/home/water/Downloads/' + +filename = f'{workdir}{pid}/{pid}_9cm_x1.obj' +bpy.ops.import_scene.obj(filepath=filename) +# 坐标复位 +obj = bpy.context.selected_objects[0] +obj.rotation_euler[0] = 0 +bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) +bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_VOLUME', center='MEDIAN') +bpy.ops.object.align(align_mode='OPT_1', relative_to='OPT_1', align_axis={'Y', 'Z'}) +bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) +# bpy.ops.export_scene.obj(filepath=f'{workdir}{pid}_align_yz.obj') + +# 躺平到打印机排版需要的坐标与角度 +obj.rotation_euler = (math.radians(90), math.radians(90), 0) +bpy.ops.object.transform_apply(rotation=True) +# bpy.ops.export_scene.obj(filepath=f'{workdir}{pid}_rotate_y90.obj') + +heights = {} +min_height = 999999 +min_i = 0 +max_height = -999999 +max_i = 0 + +bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_VOLUME', center='MEDIAN') +bpy.ops.object.align(align_mode='OPT_1', relative_to='OPT_3', align_axis={'X', 'Y', 'Z'}) + +# 步进精度2旋转X轴到180度,找到Y轴最低点和最高点,其中最低点为打印 +step = 2 +i = 0 +while i <= 180: + obj.rotation_euler = (math.radians(step), 0, 0) + bpy.ops.object.transform_apply(rotation=True) + if obj.dimensions[1] < min_height: + min_height = obj.dimensions[1] + min_i = i + if obj.dimensions[1] > max_height: + max_height = obj.dimensions[1] + max_i = i + heights[i] = (obj.dimensions[0], obj.dimensions[1], obj.dimensions[2]) + print(i, heights[i]) + i += step + +obj.rotation_euler = (0, 0, 0) +bpy.ops.object.transform_apply(rotation=True) +obj.rotation_euler = (math.radians(min_i), 0, 0) +bpy.ops.object.transform_apply(rotation=True) +#bpy.ops.export_scene.obj(filepath=f'{workdir}{pid}_miny.obj') +print(f'最小高度: {min_height} @ {heights[min_i]}min_i:{min_i}' , f'最大高度: {max_height} @ {heights[max_i]}max_i:{max_i}') + +offset = 45.5 +radian = math.radians(90) +bpy.ops.mesh.primitive_plane_add(size=200, enter_editmode=False, align='WORLD', location=(offset, 0, 0), rotation=(0, radian, 0), scale=(1, 1, 1)) + +# 布尔切割,保留交集切面 +bpy.ops.object.modifier_add(type='BOOLEAN') +bpy.context.object.modifiers["Boolean"].object = bpy.data.objects[pid] +bpy.context.object.modifiers["Boolean"].operation = 'INTERSECT' +bpy.context.object.modifiers["Boolean"].solver = 'FAST' +bpy.ops.object.modifier_apply(modifier="Boolean") + +# 拆分切割面为多个多边形,然后遍历多边形,找到最大的面积 +bpy.ops.mesh.separate(type='LOOSE') + +max_area = 0 +max_obj = None +for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.name.startswith('Plane'): + area = obj.data.polygons[0].area + if area > max_area: + max_area = area + max_obj = obj + +# 选中最大面积的多边形,然后计算中心点 +bpy.ops.object.select_all(action='DESELECT') +max_obj.select_set(True) +bpy.context.view_layer.objects.active = max_obj +bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') + +bpy.ops.import_scene.obj(filepath=f'{workdir}{pid}/qrcode.obj') +qr_obj = bpy.data.objects['qrcode'] +shore_obj = bpy.data.objects['Cube.001'] +qr_obj.location = (max_obj.location[0] - qr_obj.dimensions[0] / 2 - shore_obj.dimensions[0], max_obj.location[1], max_obj.location[2]) +shore_obj.location = (qr_obj.location[0]-0.01, max_obj.location[1], max_obj.location[2]) + +for obj in bpy.data.objects: + if obj.type == 'MESH' and obj.name.startswith('Plane'): + bpy.data.objects.remove(obj) \ No newline at end of file diff --git a/blender/fill_dm_code.py b/blender/fill_dm_code.py new file mode 100644 index 0000000..3e0e86b --- /dev/null +++ b/blender/fill_dm_code.py @@ -0,0 +1,486 @@ +import os, sys, bpy, math, time, platform, cairosvg, ppf.datamatrix, shutil, requests, json, redis, oss2, cv2 +from retrying import retry +import numpy as np +import matplotlib.pyplot as plt +from PIL import Image, ImageEnhance +from addon_utils import enable +import logging +logging.basicConfig(filename='foot_update_res.log', level=logging.ERROR) +enable('io_import_images_as_planes') +enable('eyek_addon') + +def gen_data_matrix(print_id, qr_path, size = 300): + svg = ppf.datamatrix.DataMatrix(f'p{print_id}').svg() + cairosvg.svg2png(bytestring=svg, write_to=qr_path, output_width=size, output_height=size, background_color='white') + +def active_object(obj): + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + +@retry(stop_max_attempt_number=10, wait_fixed=2000) +def down_obj_fromoss(pid, print_type=1, order_id=None): + # print_type:// 打印状态 1:正常打印 2:重打 3:加打,4: 样品 + print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 开始下载模型,若网络异常将每间隔2秒重试10次...') + if not order_id is None: + path = os.path.join(workdir, f'{pid}_{order_id}') + else: + path = os.path.join(workdir, pid) + if os.path.exists(path): + print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 已存在模型文件,删除后重新下载') + shutil.rmtree(path, ignore_errors=True) + os.makedirs(path) + + # 下载分2种情况,一种是第一次打印,下载标准{pid}.obj,{pid}.mtl,{pid}Tex1.jpg,另一种是重打或加打,obj文件名可以从oss上任意获取一个,但是mtl和jpg文件名是固定的 + res = oss_client.get_object_to_file(f'objs/print/{pid}/{pid}.mtl', os.path.join(path, f'{pid}.mtl')) + last_modified = oss_client.get_object_meta(f"objs/print/{pid}/{pid}.mtl").last_modified + print(f'mtl文件最后修改时间:{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_modified))}') + print(f'下载文件:objs/print/{pid}/{pid}.mtl,状态:{res.status}') + if oss_client.object_exists(f'objs/print/{pid}/{pid}Tex1.jpg'): + res = oss_client.get_object_to_file(f'objs/print/{pid}/{pid}Tex1.jpg', os.path.join(path, f'{pid}Tex1.jpg')) + last_modified = oss_client.get_object_meta(f"objs/print/{pid}/{pid}Tex1.jpg").last_modified + print(f'jpg文件最后修改时间:{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_modified))}') + print(f'下载文件:objs/print/{pid}/{pid}Tex1.jpg,状态:{res.status}') + else: + res = oss_client.get_object_to_file(f'objs/print/{pid}/{pid}.jpg', os.path.join(path, f'{pid}Tex1.jpg')) + last_modified = oss_client.get_object_meta(f"objs/print/{pid}/{pid}.jpg").last_modified + print(f'jpg文件最后修改时间:{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_modified))}') + print(f'下载文件:objs/print/{pid}/{pid}.jpg,状态:{res.status}') + if oss_client.object_exists(f'objs/print/{pid}/{pid}.obj'): + res = oss_client.get_object_to_file(f'objs/print/{pid}/{pid}.obj', os.path.join(path, f'{pid}.obj')) + last_modified = oss_client.get_object_meta(f"objs/print/{pid}/{pid}.obj").last_modified + print(f'obj文件最后修改时间:{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_modified))}') + print(f'下载文件:objs/print/{pid}/{pid}.obj,状态:{res.status}') + else: + prefix = f'objs/print/{pid}/' + filelist = oss2.ObjectIteratorV2(oss_client, prefix=prefix) + for file in filelist: + filename = file.key.split('/')[-1] + if filename == '': continue + if filename.endswith(f'.obj'): + res = oss_client.get_object_to_file(file.key, os.path.join(path, f'{pid}.obj')) + last_modified = oss_client.get_object_meta(file.key).last_modified + print(f'obj文件最后修改时间:{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_modified))}') + print(f'下载文件:{file.key},状态:{res.status}') + break + +def find_obj(pid, order_id): + find = False + # if not os.path.exists(os.path.join(workdir, f'{pid}_{order_id}', f'{pid}.mtl')): + # print('没有找到obj模型文件,开始下载') + down_obj_fromoss(pid, order_id=order_id) + if os.path.exists(os.path.join(workdir, f'{pid}_{order_id}', f'{pid}.jpg')): + shutil.move(os.path.join(workdir, f'{pid}_{order_id}', f'{pid}.jpg'), os.path.join(workdir, f'{pid}_{order_id}', f'{pid}Tex1.jpg')) + with open(os.path.join(workdir, f'{pid}_{order_id}', f'{pid}.mtl'), 'r') as f: + lines = f.readlines() + lines = [line.replace(f'map_Kd {pid}.jpg', f'map_Kd {pid}Tex1.jpg') for line in lines] + with open(os.path.join(workdir, f'{pid}_{order_id}', f'{pid}.mtl'), 'w') as f: + f.writelines(lines) + filelist = os.listdir(os.path.join(workdir, f'{pid}_{order_id}')) + for filename in filelist: + if f'{pid}.obj' in filename: + find = True + return filename + print('没有找到obj模型文件') + return '' + +def find_pid_objname(pid): + for obj in bpy.data.objects: + if obj.name.startswith(str(pid)): + return obj.name + +def ps_color_scale_adjustment(image, shadow=0, highlight=255, midtones=1): + ''' + 模拟 PS 的色阶调整; 0 <= Shadow < Highlight <= 255 + :param image: 传入的图片 + :param shadow: 黑场(0-Highlight) + :param highlight: 白场(Shadow-255) + :param midtones: 灰场(9.99-0.01) + :return: 图片 + ''' + if highlight > 255: + highlight = 255 + if shadow < 0: + shadow = 0 + if shadow >= highlight: + shadow = highlight - 2 + if midtones > 9.99: + midtones = 9.99 + if midtones < 0.01: + midtones = 0.01 + image = np.array(image, dtype=np.float16) + # 计算白场 黑场离差 + Diff = highlight - shadow + image = image - shadow + image[image < 0] = 0 + image = (image / Diff) ** (1 / midtones) * 255 + image[image > 255] = 255 + image = np.array(image, dtype=np.uint8) + + return image + + +def show_histogram(image, image_id, save_hist_dir, min_threshold, max_threshold): + ''' + 画出直方图展示 + :param image: 导入图片 + :param image_id: 图片id编号 + :param save_hist_dir: 保存路径 + :param min_threshold: 最小阈值 + :param max_threshold: 最大阈值 + :return: 原图image,和裁剪原图直方图高低阈值后的图片image_change + ''' + plt.rcParams['font.family'] = 'SimHei' + plt.rcParams['axes.unicode_minus'] = False + plt.hist(image.ravel(), 254, range=(2, 256), density=False) + plt.hist(image.ravel(), 96, range=(2, 50), density=False) # 放大 range(0, 50),bins值最好是range的两倍,显得更稀疏,便于对比 + plt.hist(image.ravel(), 110, range=(200, 255), density=False) # 放大 range(225, 255) + plt.annotate('thresh1=' + str(min_threshold), # 文本内容 + xy=(min_threshold, 0), # 箭头指向位置 # 阈值设定值! + xytext=(min_threshold, 500000), # 文本位置 # 阈值设定值! + arrowprops=dict(facecolor='black', width=1, shrink=5, headwidth=2)) # 箭头 + plt.annotate('thresh2=' + str(max_threshold), # 文本内容 + xy=(max_threshold, 0), # 箭头指向位置 # 阈值设定值! + xytext=(max_threshold, 500000), # 文本位置 # 阈值设定值! + arrowprops=dict(facecolor='black', width=1, shrink=5, headwidth=2)) # 箭头 + # 在y轴上绘制一条直线 + # plt.axhline(y=10000, color='r', linestyle='--', linewidth=0.5) + plt.title(str(image_id)) + # plt.show() + # 保存直方图 + save_hist_name = os.path.join(save_hist_dir, f'{image_id}_{min_threshold}&{max_threshold}.jpg') + plt.savefig(save_hist_name) + # 清空画布, 防止重叠展示 + plt.clf() + + +def low_find_histogram_range(image, target_frequency): + ''' + 循环查找在 target_frequency (y)频次限制下的直方图区间值(x) + :param image: 导入图片 + :param target_frequency: 直方图 y 频次限制条件 + :return: 直方图区间 x,和 该区间频次 y + ''' + # 计算灰度直方图 + hist, bins = np.histogram(image, bins=256, range=[0, 256]) + # 初始化区间和频次 + interval = 2 + frequency = hist[255] + while frequency < target_frequency: + # 更新区间和频次 + interval += 1 + # 检查直方图的频次是否为None,如果频次是None,则将其设为0,这样可以避免将None和int进行比较报错。 + frequency = hist[interval] if hist[interval] is not None else 0 + frequency += hist[interval] if hist[interval] is not None else 0 + # 如果频次接近10000则停止循环 + if target_frequency - 2000 <= frequency <= target_frequency + 1000: + break + + return interval, frequency + + +def high_find_histogram_range(image, target_frequency): + ''' + 循环查找在 target_frequency (y)频次限制下的直方图区间值(x) + :param image: 导入图片 + :param target_frequency: 直方图 y 频次限制条件 + :return: 直方图区间 x,和 该区间频次 y + ''' + # 计算灰度直方图 + hist, bins = np.histogram(image, bins=256, range=[0, 256]) + # 初始化区间和频次 + interval = 255 + frequency = hist[255] + while frequency < target_frequency: + # 更新区间和频次 + interval -= 1 + # 检查直方图的频次是否为None,如果频次是None,则将其设为0,这样可以避免将None和int进行比较报错。 + frequency = hist[interval] if hist[interval] is not None else 0 + frequency += hist[interval] if hist[interval] is not None else 0 + # 如果频次接近10000则停止循环 + if target_frequency - 2000 <= frequency <= target_frequency + 2000: + break + + return interval, frequency + +def reduce_sharpness(image, factor): + ''' + 使用PIL库减弱图像锐度 + :param image: 图像 + :param factor: 锐度因子,0表示最大程度减弱锐度,1表示原始图像 + :return: 减弱锐度后的图像 + ''' + # OpenCV 格式的图像转换为 PIL 的 Image 对象 + image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + pil_image = Image.fromarray(image_rgb) + enhancer = ImageEnhance.Sharpness(pil_image) + reduced_image = enhancer.enhance(factor) + # PIL 的 Image 对象转换为 OpenCV 的图像格式 + image_array = np.array(reduced_image) + sharpened_image = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR) + + return sharpened_image + +def sharpening_filter(image): + ''' + 锐化滤波器对图片进行锐化,增强图像中的边缘和细节 + :param image: 导入图片 + :return: 锐化后的图片 + ''' + sharp_kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]]) + sharpened_image = cv2.filter2D(image, -1, sharp_kernel) + return sharpened_image + +def find_last_x(image, slope_threshold = 1000): + x = [] + y = [] + hist, bins = np.histogram(image, bins=256, range=[0, 256]) + + #找到50以内的最高峰 + max_y = 0 + max_i = 5 + for i in range(5, 50): + if hist[i] > max_y: + max_y = hist[i] + max_i = i + print(f'50以内最高峰值y:{max_y},最高峰位置x:{max_i}') + + for i in range(2, max_i): + x.append(i) + y.append(hist[i]) + slopes = [abs(y[i + 1] - y[i]) for i in range(len(x) - 1)] + + current_interval = [] + max_interval = [] + max_x = {} + for i, slope in enumerate(slopes): + current_interval.append(slope) + if slope >= slope_threshold: + if len(current_interval) > len(max_interval): + max_interval = current_interval.copy() + max_x[x[i]] = slope + current_interval = [] + + print(max_x) + last_x = list(max_x)[-1] + last_y = max_x[last_x] + return last_x, last_y + +def remove_gray_and_sharpening(jpg_path): + input_image = cv2.imread(jpg_path) + # low_x_thresh, low_y_frequency = low_find_histogram_range(input_image, low_y_limit) + low_x_thresh, low_y_frequency = find_last_x(input_image, 1000) + high_x_thresh, high_y_frequency = high_find_histogram_range(input_image, high_y_limit) + print(f"{low_x_thresh} 区间, {low_y_frequency} 频次") + print(f"{high_x_thresh} 区间, {high_y_frequency} 频次") + high_output_image = ps_color_scale_adjustment(input_image, shadow=low_x_thresh, highlight=high_x_thresh, midtones=1) + # high_output_image = ps_color_scale_adjustment(low_ouput_image, shadow=0, highlight=high_x_thresh, midtones=1) + + # # 人体贴图和黑色背景交界处不进行锐化 + # gray = cv2.cvtColor(input_image, cv2.COLOR_BGR2GRAY) + # _, thresh = cv2.threshold(gray, 2, 255, cv2.THRESH_BINARY) + # kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7)) + # gradient = cv2.morphologyEx(thresh, cv2.MORPH_GRADIENT, kernel) + # roi_gradient = cv2.bitwise_and(high_output_image, high_output_image, mask=gradient) + + # # 锐化滤波器 + # # sharpened_image = sharpening_filter(high_output_image) + # sharpened_image = reduce_sharpness(high_output_image, factor=4) + # # 将原图边界替换锐化后的图片边界 + # sharpened_image[gradient != 0] = roi_gradient[gradient != 0] + + # 直方图标记并保存 + # show_histogram(input_image, img_id, low_x_thresh, high_x_thresh) + cv2.imwrite(jpg_path, high_output_image, [cv2.IMWRITE_JPEG_QUALITY, 95]) # 保存图片的质量是原图的 95% + +def main(workdir, r, print_id): + print('脚底板二维码程序开始运行...') + only_one = False + while True: + if print_id == '0': + try: + if r.llen('model:foot') == 0: + # print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), '队列为空,5秒后重试') + time.sleep(5) + continue + except Exception as e: + print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), 'redis连接异常,5秒后重试') + print(e) + time.sleep(5) + r = redis.Redis(host='106.14.158.208', password='kcV2000', port=6379, db=6) + # r = redis.Redis(host='172.31.1.254', password='', port=6379, db=6) + continue + # 打印队列里面的全部内容 + print(f'当前model:foot队列长度:{r.llen("model:foot")}') + for i in r.lrange('model:foot', 0, -1): + print(i) + print_id = r.lpop('model:foot') + if print_id is None: + print_id = '0' + continue + print_id = print_id.decode('utf-8') + else: + print(f'接收到运行一个{print_id}任务') + only_one = True + + res = requests.get(f'{get_pid_by_printid_url}?print_id={print_id}') + print('获取pid:', f'{get_pid_by_printid_url}?print_id={print_id}', res.text) + pid = json.loads(res.text)['data']['pid'] + order_id = json.loads(res.text)['data']['order_id'] + + filename = os.path.join(workdir, f'{pid}_{order_id}', find_obj(pid, order_id)) + print('导入obj文件:', filename) + + if only_one: + print(f'接收到运行一个{print_id}任务,强制调用cal_foot_position.py计算并上传qr_position') + os.system(f'blender -b -P cal_foot_position.py -- {pid}_{order_id}_{print_id}') + res = requests.get(f'{get_qr_position_url}?print_id={print_id}') + print('从云端获取的qr_position:', res.text) + qr_position = json.loads(res.text)['data']['position_data'] + else: + #从云端获取qr_position,如果获取为空,调用cal_foot_position.py计算并上传qr_position,再重新读取qr_position.txt + res = requests.get(f'{get_qr_position_url}?print_id={print_id}') + print('从云端获取的qr_position:', res.text) + + qr_position = json.loads(res.text)['data']['position_data'] + + if qr_position == '': + print('qr_position为空,调用cal_foot_position.py计算并上传qr_position') + os.system(f'blender -b -P cal_foot_position.py -- {pid}_{order_id}_{print_id}') + res = requests.get(f'{get_qr_position_url}?print_id={print_id}') + print('从云端获取的qr_position:', res.text) + qr_position = json.loads(res.text)['data']['position_data'] + else: + qr_position = json.loads(qr_position) + + if type(qr_position) == str: qr_position = json.loads(qr_position) + print(f'type of qr_position:{type(qr_position)}') + print(f'qr_position:{qr_position}') + + qr_position['location'][2] = -0.1 + # 根据print_id生成qr码 + qr_path = os.path.join(workdir, f'{pid}_{order_id}' ,'qr.png') + gen_data_matrix(print_id, qr_path) + + # 导入obj文件,重置到标准单位 + bpy.ops.wm.read_homefile() + bpy.context.preferences.view.language = 'en_US' + bpy.ops.object.delete(use_global=False, confirm=False) + bpy.context.scene.unit_settings.scale_length = 0.001 + bpy.context.scene.unit_settings.length_unit = 'CENTIMETERS' + bpy.context.scene.unit_settings.mass_unit = 'GRAMS' + + bpy.ops.import_scene.obj(filepath=filename) + + obj = bpy.context.selected_objects[0] + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + pid_objname = find_pid_objname(pid) + + scale = 90 / obj.dimensions.y + obj.scale = (scale, scale, scale) + bpy.ops.object.align(align_mode='OPT_1', relative_to='OPT_1', align_axis={'Z'}) + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') + obj.location[0] = 0 + obj.location[1] = 0 + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + print(f'qr_position:{qr_position}') + print(f'qr_position_type:{type(qr_position)}') + + # 根据qr_position的值,恢复qr的位置和尺寸,重新生成贴图 + bpy.ops.object.load_reference_image(filepath=os.path.join(workdir, f'{pid}_{order_id}', 'qr.png')) + bpy.context.object.rotation_euler = (math.radians(-180), math.radians(0), qr_position['rotation']) + bpy.ops.transform.translate(value=qr_position['location'], orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False, snap=False, snap_elements={'INCREMENT'}, use_snap_project=False, snap_target='CLOSEST', use_snap_self=True, use_snap_edit=True, use_snap_nonedit=True, use_snap_selectable=False, release_confirm=True) + + bpy.context.object.empty_display_size = qr_position['dimensions'][0] + + qr_path = os.path.join(workdir,f'{pid}_{order_id}', f"{pid}_{order_id}Tex1_qr.png") + jpg_path = os.path.join(workdir,f'{pid}_{order_id}', f"{pid}Tex1.jpg") + jpg_img = Image.open(jpg_path) + shutil.copyfile(jpg_path, os.path.join(workdir,f'{pid}_{order_id}', f"{pid}Tex1_noqr.jpg")) + + bpy.context.scene.eyek.res_x = jpg_img.width + bpy.context.scene.eyek.res_y = jpg_img.height + bpy.context.scene.eyek.path_export_image = qr_path + bpy.data.objects[pid_objname].select_set(True) + bpy.data.objects['Empty'].select_set(True) + bpy.context.view_layer.objects.active = bpy.data.objects[pid_objname] + bpy.ops.eyek.exe() + + qr_img = Image.open(qr_path) + jpg_img.paste(qr_img, (0, 0), qr_img) + jpg_img.save(jpg_path, quality=90) + shutil.copyfile(jpg_path, os.path.join(workdir,f'{pid}_{order_id}', f"{pid}Tex1_qr.jpg")) + + # 加入去灰、锐化 + remove_gray_and_sharpening(jpg_path) + + upload_jpg_mtl(pid, order_id, print_id) + + # plt.axis('equal') + # plt.show() + + # 保存blend文件 + # bpy.ops.wm.save_as_mainfile(filepath=f'{workdir}{pid}_{order_id}/{pid}_qr_end.blend') + bpy.ops.wm.quit_blender() + + # 删除临时文件 + shutil.rmtree(os.path.join(workdir, f'{pid}_{order_id}')) + if only_one: + print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 运行{print_id}任务完成,退出程序') + break + else: + print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 运行{print_id}任务完成,继续运行下一个任务') + print_id = '0' + continue + +@retry(stop_max_attempt_number=10, wait_fixed=2000) +def upload_jpg_mtl(pid, order_id, print_id): + print('生成贴图完成,开始上传...') + oss_client.put_object_from_file(f'objs/print/{pid}/{pid}Tex1.{print_id}.jpg', os.path.join(workdir,f'{pid}_{order_id}', f"{pid}Tex1.jpg")) + oss_client.put_object_from_file(f'objs/print/{pid}/{pid}.mtl', os.path.join(workdir,f'{pid}_{order_id}', f"{pid}.mtl")) + # oss_client.put_object_from_file(f'objs/print/{pid}/{pid}Tex1_noqr.jpg', os.path.join(workdir,f'{pid}_{order_id}', f"{pid}Tex1_noqr.jpg")) + + print('更新状态为已生成脚底板二维码') + res = requests.post(f'{upload_qr_position_url}?print_id={print_id}') + #记录日志 + logging.error(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}-结果:pid:{pid}-print_id:{print_id} {str(res.text)}") + print('更新返回状态:', f'{upload_qr_position_url}?print_id={print_id}', res.text) + +if __name__ == '__main__': + low_y_limit = 25000 + high_y_limit = 13000 + + get_qr_position_url = 'https://mp.api.suwa3d.com/api/printOrder/getFootCodePositionData' + upload_qr_position_url = 'https://mp.api.suwa3d.com/api/printOrder/updateFootCodeStatus' + get_pid_by_printid_url = 'https://mp.api.suwa3d.com/api/printOrder/getPidByPrintId' + # get_qr_position_url = 'http://172.31.1.254:8199/api/printOrder/getFootCodePositionData' + # upload_qr_position_url = 'http://172.31.1.254:8199/api/printOrder/updateFootCodeStatus' + # get_pid_by_printid_url = 'http://172.31.1.254:8199/api/printOrder/getPidByPrintId' + + + r = redis.Redis(host='106.14.158.208', password='kcV2000', port=6379, db=6) + # r = redis.Redis(host='172.31.1.254', password='', port=6379, db=6) + AccessKeyId = 'LTAI5tSReWm8hz7dSYxxth8f' + AccessKeySecret = '8ywTDF9upPAtvgXtLKALY2iMYHIxdS' + Endpoint = 'oss-cn-shanghai.aliyuncs.com' + Bucket = 'suwa3d-securedata' + oss_client = oss2.Bucket(oss2.Auth(AccessKeyId, AccessKeySecret), Endpoint, Bucket) + + if platform.system() == 'Windows': + workdir = 'd:\\print\\' + else: + workdir = '/data/datasets/foot/' + + print("Usage: blender -b -P fill_dm_code.py") + + if len(sys.argv) == 5: + print_ids = sys.argv[-1] + else: + print_ids = '0' + + for print_id in print_ids.split(','): + main(workdir, r, print_id) + + diff --git a/blender/resize_model.py b/blender/resize_model.py new file mode 100644 index 0000000..909044e --- /dev/null +++ b/blender/resize_model.py @@ -0,0 +1,176 @@ +from math import radians +import sys, platform, os, time, bpy, requests, json, bmesh, shutil +from PIL import Image +import platform +if platform.system() == 'Windows': + sys.path.append('e:\\libs\\') +else: + sys.path.append('/data/deploy/make3d/make2/libs/') +import config, libs + +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 fix_link_texture(pid): + # 修改obj中的mtl文件为pid_original.mtl + path = os.path.join(workdir, 'print', f'{pid}_{orderId}') + filename = os.path.join(path, f'{pid}_original.obj') + + with open(filename, 'r') as f: + lines = f.readlines() + for i in range(len(lines)): + if lines[i].startswith('mtllib'): + lines[i] = f'mtllib {pid}_original.mtl\n' + break + with open(filename, 'w') as f: + f.writelines(lines) + + f.close() + + # 将pid.mtl文件复制为pid_original.mtl _decimate + shutil.copy(os.path.join(path, f'{pid}.mtl'), os.path.join(path, f'{pid}_original.mtl')) + shutil.copy(os.path.join(path, f'{pid}Tex1.jpg'), os.path.join(path, f'{pid}Tex1_decimate.jpg')) + texture_file = os.path.join(path, f'{pid}Tex1_decimate.jpg') + if os.path.exists(texture_file): + img = Image.open(texture_file) + img = img.resize((int(img.size[0] * 0.5), int(img.size[1] * 0.5))) + img.save(texture_file, quality=90, optimize=True) + print('resize texture file to 50% success') + # 修改pid_original.mtl文件中的贴图为pid_old.jpg + with open(os.path.join(path, f'{pid}_original.mtl'), 'r') as f: + lines = f.readlines() + for i in range(len(lines)): + if lines[i].startswith('map_Kd'): + lines[i] = f'map_Kd {pid}Tex1_decimate.jpg\n' + break + with open(os.path.join(path, f'{pid}_original.mtl'), 'w') as f: + f.writelines(lines) + + f.close() + + +def main(): + start = time.time() + + get_printsize_url = 'https://mp.api.suwa3d.com/api/printOrder/info' + upload_obj_volume_url = 'https://mp.api.suwa3d.com/api/physical/add' # ?pid=1&order_id=1&faces=1&volume=1 + + + res = requests.get(f'{get_printsize_url}?id={orderId}') + print('获取打印尺寸:', res.text) + + if res.status_code == 200: + pid = res.json()['data']['pid'] + path = os.path.join(workdir, 'print', f'{pid}_{orderId}') + filename = os.path.join(path, f'{pid}.obj') + bpy.ops.object.delete(use_global=False, confirm=False) + print('正在处理:', filename) + bpy.ops.import_scene.obj(filepath=filename) + obj = bpy.context.selected_objects[0] + print('原始模型尺寸:', obj.dimensions) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + print('应用后模型尺寸:', obj.dimensions) + shutil.copy(filename, os.path.join(path, f'{pid}_original.obj')) + filename_original = os.path.join(path, f'{pid}_original.obj') + + for f in res.json()['data']['fileList']: + height = float(f.split('_')[-2][:-2]) * 10 + + obj = bpy.context.selected_objects[0] + print(f'{f}处理前{height}mm模型尺寸: {obj.dimensions}') + scale = height / obj.dimensions.z + obj.scale = (scale, scale, scale) + bpy.ops.object.transform_apply(scale=True) + print(f'{f}处理后{height}mm模型尺寸: {obj.dimensions}') + + bpy.ops.export_scene.obj(filepath=os.path.join(path, f'{pid}.obj')) + if os.path.exists(os.path.join(path, f)): + os.remove(os.path.join(path, f)) + os.rename(os.path.join(path, f'{pid}.obj'), os.path.join(path, f)) + config.oss_bucket.put_object_from_file(f'objs/print/{pid}/{f}', os.path.join(path, f)) + + # 重新加载模型,然后生成数字模型 + bpy.ops.object.delete(use_global=False, confirm=False) + fix_link_texture(pid) + bpy.ops.import_scene.obj(filepath=filename_original) + + obj = bpy.context.selected_objects[0] + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + if pid == '85964': bpy.ops.wm.save_as_mainfile(filepath=os.path.join(path, f'{pid}_{orderId}.blend')) + + scale = 90 / obj.dimensions.z + obj.scale = (scale, scale, scale) + + headcount = res.json()['data']['headcount'] + + bm = bmesh_copy_from_object(obj) + obj_volume = round(bm.calc_volume() / 1000, 3) + print('volume:', obj_volume) + print('weight:', obj_volume * 1.2, 'g') + + faces = len(obj.data.polygons) + print('faces:', faces) + upload_res = requests.get(f'{upload_obj_volume_url}?pid={pid}&order_id={orderId}&faces={faces}&volume={obj_volume}&headcount={headcount}') + print('上传模型体积:', upload_res.text) + + # 生成数字模型 + + faces_dest = 120000 * headcount + # 减面 + faces_current = len(obj.data.polygons) + bpy.ops.object.modifier_add(type='DECIMATE') + bpy.context.object.modifiers["Decimate"].ratio = faces_dest / faces_current + bpy.ops.object.modifier_apply(modifier="Decimate") + + 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) + + bpy.ops.export_scene.obj(filepath=os.path.join(path, f'{pid}_decimate.obj')) + bpy.ops.export_scene.gltf(filepath=os.path.join(path, f'{pid}_decimate.glb'), export_format='GLB', export_apply=True, export_jpeg_quality=80) + config.oss_bucket.put_object_from_file(f'glbs/3d/{pid}.glb', os.path.join(path, f'{pid}_decimate.glb')) + bpy.ops.wm.quit_blender() + + +if __name__ == '__main__': + if platform.system() == 'Windows': + workdir = 'd:\\' + else: + workdir = '/data/datasets/' + + if len(sys.argv) - (sys.argv.index("--") + 1) < 1: + print("Usage: blender -b -P resize_model.py -- ") + sys.exit(1) + orderId = sys.argv[sys.argv.index("--") + 1] + + main() diff --git a/install.txt b/install.txt deleted file mode 100644 index 8cfb0f6..0000000 --- a/install.txt +++ /dev/null @@ -1,13 +0,0 @@ -pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple -pip config set install.trusted-host pypi.tuna.tsinghua.edu.cn - -python -m pip install --upgrade pip -pip install oss2 redis pillow numpy opencv-python bpy tqdm pyautogui psutil pywin32 pymysql - - -config -set bin="C:\Program Files\Capturing Reality\RealityCapture\RealityCapture.exe" -%bin% -disableOnlineCommunication -setInstanceName %pid% -%bin% -disableOnlineCommunication -delegateTo %pid% -%bin% -set "appCacheLocation=ProjectFolder" - diff --git a/libs/common.py b/libs/common.py index e5094d1..59b149c 100644 --- a/libs/common.py +++ b/libs/common.py @@ -113,6 +113,28 @@ def change_rcbox_s(pid,new_value): with open(rcbox_path, 'w') as f: f.write(new_content) +#读取 rcbox 修改 widthHeightDepth 重建区域的深度 +def change_rcbox_deepth(pid,new_value): + rcbox_path = os.path.join(config.workdir, pid, f"{pid}.rcbox") + old_value_pattern = r'(.*?)' + #读取文件内容 + with open(rcbox_path, 'r') as f: + content = f.read() + #使用正则表达式进行匹配 + match = re.search(old_value_pattern,content) + if match: + old_value = match.group(1) + if old_value == "": + return + #分割字符串 + arrStr = old_value.split(" ") + #重新拼接字符串 + strs = arrStr[0]+" "+arrStr[1]+" "+str(float(arrStr[2])+new_value) + new_content = re.sub(old_value_pattern,f'{strs}',content) + #重新写入进去 + with open(rcbox_path, 'w') as f: + f.write(new_content) + #修改rcproj文件,删除没有模型的component,保留最多model 的component def changeRcprojFile(pid): # 解析XML文件 @@ -180,6 +202,7 @@ def down_points_from_oss(pid,psid): #判断oss上是否存在指定的controlpoints文件 def isExistControlPointsOss(pid): + return False psid = libs.getPSid(pid) filePath = f'points/{psid}/controlpoints_{psid}.dat' #判断oss上是否存在 diff --git a/libs/libs_db.py b/libs/libs_db.py index 7b96304..19c96ba 100644 --- a/libs/libs_db.py +++ b/libs/libs_db.py @@ -149,7 +149,7 @@ def isStudioConfigDistribute(psid): try: with pymysqlAlias() as conn: cursor = conn.cursor() - sql = f'select count(*) from studio_config_distribute where studio_id = {psid}' + sql = f'select count(*) from studio_config_distribute where studio_id = {psid} and status = 1' cursor.execute(sql) result = cursor.fetchone() if result[0] == 0: diff --git a/libs/main_service_db.py b/libs/main_service_db.py index 107c2a2..9849e79 100644 --- a/libs/main_service_db.py +++ b/libs/main_service_db.py @@ -185,4 +185,24 @@ def update_task_distributed_detail(data): except Exception as e: print(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())} 执行update_task_distributed_detail({data})异常: {str(e)}") logging.error(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())} 执行update_task_distributed_detail({data})异常: {str(e)}") - return "error" \ No newline at end of file + return "error" + +#获取需要执行step1的任务 +def get_task_distributed_step1(): + try: + with pymysqlAlias() as conn: + cursor = conn.cursor(pymysql.cursors.DictCursor) + sql = 'select * from task_distributed where status =0 order by priority desc limit 1' + if where: + sql += f' and {where}' + + cursor.execute(sql) + result = cursor.fetchone() + # 关闭游标和连接 + ##cursor.close() + #conn.close() + return result + except Exception as e: + print(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())} 执行get_task_distributed_step1()异常: {str(e)}") + logging.error(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())} 执行get_task_distributed_step1()异常: {str(e)}") + return 'error' \ No newline at end of file diff --git a/logic/logic_main_service.py b/logic/logic_main_service.py index 6d95762..c4bafcd 100644 --- a/logic/logic_main_service.py +++ b/logic/logic_main_service.py @@ -28,10 +28,10 @@ def get_task_distributed(): print("获取需要执行的步骤 next_step",next_step_result) return next_step_result #非R11 R12 的主机在执行step2的时候,需要判断当前模型是否需要高精模或者photo3参与建模,如果是的话,该主机不执行这一步 - if next_step_result["run_step"] == "step2": - if common.task_need_high_model_or_photo3(next_step_result["task_key"]): - print(f'模型{next_step_result["task_key"]}需要高精模或者photo3参与建模,该主机{hostname}不执行step2') - return "no" + # if next_step_result["run_step"] == "step2": + # if common.task_need_high_model_or_photo3(next_step_result["task_key"]): + # print(f'模型{next_step_result["task_key"]}需要高精模或者photo3参与建模,该主机{hostname}不执行step2') + # return "no" #taskData = next_step_result #{"hostname":hostname,"run_step":next_step,"task_distributed_id":result["id"],"task_key":result["task_key"]} flagRes = update_main_and_add_detail(next_step_result) if flagRes == "error": @@ -42,24 +42,27 @@ def get_task_distributed(): else: return 'no_data' else: - #R11 R12的主机 可以执行step1 2 3 的任务 - #如果R11 R12的主机目前没有正在执行step2,则优先处理step2, - # print("次数",is_run_stepx_nums("step2")) - if is_run_stepx_nums("step2") < 0: - resultData = need_run_step2() - if resultData != "no": - resultData["hostname"] = hostname - flagRes = update_main_and_add_detail(resultData) - if flagRes == "error": - print(f'出现错误,有可能是多个进程获取同一个任务了,重新获取任务去执行了') - return "error" - print(f'任务ID-{resultData["task_key"]}- "执行step2" ') - return resultData - + #优先处理step1 和 step3 #R11 R12的主机如果已经有在处理step2了,则不能再处理step2,只能处理step1 step3 resultData = need_run_step_no_step2() if resultData == "no": + #return "no" + #R11 R12的主机 可以执行step1 2 3 的任务 + #如果R11 R12的主机目前没有正在执行step2,则优先处理step2, + # print("次数",is_run_stepx_nums("step2")) + if is_run_stepx_nums("step2") < 2: + resultData = need_run_step2() + if resultData != "no": + resultData["hostname"] = hostname + flagRes = update_main_and_add_detail(resultData) + if flagRes == "error": + print(f'出现错误,有可能是多个进程获取同一个任务了,重新获取任务去执行了') + return "error" + print(f'任务ID-{resultData["task_key"]}- "执行step2" ') + return resultData + #没有任何可执行的 return "no" + resultData["hostname"] = hostname flagRes = update_main_and_add_detail(resultData) if flagRes == "error": @@ -119,7 +122,7 @@ def need_run_stepx(task_distributed_id): return "step3" elif result["step"] == "step3": #这里要将 主任务表的状态改为2,finished_at改为当前时间 - update_task_distributed({"id":task_distributed_id,"status":2,"finished_at":time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()),"step_last":"step3"}) + main_service_db.update_task_distributed({"id":task_distributed_id,"status":2,"finished_at":time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()),"step_last":"step3"}) return "no" return "no" except Exception as e: @@ -180,7 +183,7 @@ def need_run_step_no_step2(): #判断是否有正在执行的step2 xstep = need_run_stepx(row["id"]) # print("查询非step2的任务列表",xstep) - if xstep != "step2" and (xstep == "step1" or xstep == "step3"): + if xstep != "step2" and (xstep == "step1"): #没有正在执行的step2,则返回该任务 return {"hostname":hostname,"run_step":xstep,"task_distributed_id":row["id"],"task_key":row["task_key"]} return "no" @@ -201,7 +204,9 @@ def need_run_step_no_step1(): #判断是否有正在执行的step2 xstep = need_run_stepx(row["id"]) #print("查询非step1的任务列表",xstep,row["id"]) - if xstep == "step2" or xstep == "step3": + if xstep == "step2": + if common.task_need_high_model_or_photo3(row["task_key"]) and hostname != "R11" and hostname != "R12": + continue #没有正在执行的step1,则返回该任务 return {"hostname":hostname,"run_step":xstep,"task_distributed_id":row["id"],"task_key":row["task_key"]} return "no" diff --git a/main_step1.py b/main_step1.py index 17e39cc..8cf89d7 100644 --- a/main_step1.py +++ b/main_step1.py @@ -109,7 +109,6 @@ def cal_reconstruction_region(psid, pid): print(cmd) cmd = shlex.split(cmd) res = subprocess.run(cmd) - fix_region() print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 重建区域计算完成') diff --git a/main_step2.py b/main_step2.py index 603dd33..3d193df 100644 --- a/main_step2.py +++ b/main_step2.py @@ -24,6 +24,10 @@ def make3d(pid): if get_rcver() == 1: # old version #修改重建区域的大小 common.change_rcbox_s(pid,"0.999") + #获取影棚id + # psid = libs.getPSid(pid) + # if int(psid) == 80: + # change_rcbox_deepth(str(pid),0.03) simplify_value = 1000000 * libs.getHeadCount(pid) add_photo3 = ' ' @@ -112,21 +116,6 @@ def make3d(pid): cmd = shlex.split(cmd) res = subprocess.run(cmd) - #阻塞判断是否导出完成 - while True: - #判断 output 目录下是否存在 三个文件 - files = os.listdir(os.path.join(config.workdir, pid, "output")) - if len(files) >= 3: - break - - - #2023-10-27为了解决老版本使用step1 的 重建区域框的问题,这里加入了 -set "sfmEnableCameraPrior=True" -align -set "sfmEnableCameraPrior=False" align 使相机的对齐线统一向下后,再进行重建区域的设置 - # cmd = f'{config.rcbin} {config.r1["init"]} -load "{os.path.join(config.workdir, pid, f"{pid}.rcproj")}" -update \ - # -set "sfmEnableCameraPrior=True" -align -set "sfmEnableCameraPrior=False" -align -setReconstructionRegion "{os.path.join(config.workdir, pid, f"{pid}.rcbox")}" \ - # -mvs -modelSelectMaximalConnectedComponent -renameModel {pid} -modelInvertSelection -modelRemoveSelectedTriangles -closeHoles -clean -simplify {simplify_value} -smooth -unwrap -calculateTexture -save "{os.path.join(config.workdir, pid, f"{pid}.rcproj")}" -exportModel "{pid}" "{os.path.join(config.workdir, pid, "output", f"{pid}.obj")}" "d:\\make2\\config\\ModelExportParams102.xml" -quit' - # print(cmd) - # cmd = shlex.split(cmd) - # res = subprocess.run(cmd) else: # new version @@ -135,6 +124,11 @@ def make3d(pid): calulate_type = 'calculateHighModel' else: calulate_type = 'calculateNormalModel' + + #创建指定文件夹 + if not os.path.exists(os.path.join(config.workdir, pid, "output")): + os.makedirs(os.path.join(config.workdir, pid, "output")) + cmd = f'{config.rcbin} {config.r2["init"]} -setInstanceName {pid} \ -load "{os.path.join(config.workdir, pid, f"{pid}.rcproj")}" \ -{calulate_type} \ @@ -145,6 +139,14 @@ def make3d(pid): cmd = shlex.split(cmd) res = subprocess.run(cmd) + + #阻塞判断是否导出完成 + while True: + #判断 output 目录下是否存在 三个文件 + files = os.listdir(os.path.join(config.workdir, pid, "output")) + if len(files) >= 3: + break + def step2(pid,task_distributed_id=""): print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 开始建模任务step2') diff --git a/tools/RDP.bat b/tools/RDP.bat new file mode 100644 index 0000000..c9dffba --- /dev/null +++ b/tools/RDP.bat @@ -0,0 +1,3 @@ +for /f "skip=1 tokens=3" %%s in ('query user %USERNAME%') do ( + %windir%\System32\tscon.exe %%s /dest:console +) \ No newline at end of file diff --git a/tools/auto_distance.py b/tools/auto_distance.py index ac4e211..ec2434e 100644 --- a/tools/auto_distance.py +++ b/tools/auto_distance.py @@ -53,7 +53,7 @@ def get_defineDistances(pid, left, top, right, bottom): psid = libs.getPSid(pid) distances = libs_db.get_floor_sticker_distances(psid) #config.ps_floor_sticker.get(psid, config.ps_floor_sticker['default']) time.sleep(5) - y = 748 + y = 951 yCreateDistance = 0 if len(distances.split(';')) == 1: yCreateDistance = 2 diff --git a/tools/gen_xmps.py b/tools/gen_xmps.py index 6d6b532..b307fb4 100644 --- a/tools/gen_xmps.py +++ b/tools/gen_xmps.py @@ -9,13 +9,17 @@ import config, libs, libs_db def upload_xmp(pid): print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 上传xmp文件之前先删除oss上的xmp文件所在目录...') + pid = str(pid) psid = libs.getPSid(pid) #移除掉旧的文件夹 #config.oss_bucket.delete_object(f'xmps/{pid}/') #删除oss 上的文件夹里的内容 - object_list = oss2.ObjectIterator(config.oss_bucket, prefix=f'xmps/{psid}/') - if not any(object_list): - config.oss_bucket.batch_delete_objects([obj.key for obj in object_list]) + + #判断是否存在该目录 + if config.oss_bucket.object_exists(f'xmps/{psid}/') == True: + object_list = oss2.ObjectIterator(config.oss_bucket, prefix=f'xmps/{psid}/') + if not any(object_list): + config.oss_bucket.batch_delete_objects([obj.key for obj in object_list]) start_time = time.time() workdir = os.path.join(config.workdir, pid)