Browse Source

分布式处理和脚底板文件

master
dongchangxi 2 years ago
parent
commit
2c036235a6
  1. 148
      blender/auto_qrcode.py
  2. 160
      blender/autofix.py
  3. 144
      blender/autofix10.py
  4. 628
      blender/cal_foot_position.py
  5. BIN
      blender/controlpoints0.dat
  6. BIN
      blender/controlpoints_80.dat
  7. 91
      blender/debug.Text.py
  8. 486
      blender/fill_dm_code.py
  9. 176
      blender/resize_model.py
  10. 13
      install.txt
  11. 23
      libs/common.py
  12. 2
      libs/libs_db.py
  13. 20
      libs/main_service_db.py
  14. 47
      logic/logic_main_service.py
  15. 1
      main_step1.py
  16. 32
      main_step2.py
  17. 3
      tools/RDP.bat
  18. 2
      tools/auto_distance.py
  19. 10
      tools/gen_xmps.py

148
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 -- <pid>")
sys.exit(1)
pid = sys.argv[sys.argv.index("--") + 1]
main()

160
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 -- <pids>")
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()

144
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 -- <pids>")
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()

628
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 -- <pid_order_id_print_id>")
sys.exit(1)
pid, order_id, print_id = sys.argv[sys.argv.index("--") + 1].split('_')
main(workdir, pid, order_id, print_id)

BIN
blender/controlpoints0.dat

Binary file not shown.

BIN
blender/controlpoints_80.dat

Binary file not shown.

91
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)

486
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)

176
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 -- <orderId>")
sys.exit(1)
orderId = sys.argv[sys.argv.index("--") + 1]
main()

13
install.txt

@ -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"

23
libs/common.py

@ -113,6 +113,28 @@ def change_rcbox_s(pid,new_value):
with open(rcbox_path, 'w') as f: with open(rcbox_path, 'w') as f:
f.write(new_content) 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'<widthHeightDepth>(.*?)</widthHeightDepth>'
#读取文件内容
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'<widthHeightDepth>{strs}</widthHeightDepth>',content)
#重新写入进去
with open(rcbox_path, 'w') as f:
f.write(new_content)
#修改rcproj文件,删除没有模型的component,保留最多model 的component #修改rcproj文件,删除没有模型的component,保留最多model 的component
def changeRcprojFile(pid): def changeRcprojFile(pid):
# 解析XML文件 # 解析XML文件
@ -180,6 +202,7 @@ def down_points_from_oss(pid,psid):
#判断oss上是否存在指定的controlpoints文件 #判断oss上是否存在指定的controlpoints文件
def isExistControlPointsOss(pid): def isExistControlPointsOss(pid):
return False
psid = libs.getPSid(pid) psid = libs.getPSid(pid)
filePath = f'points/{psid}/controlpoints_{psid}.dat' filePath = f'points/{psid}/controlpoints_{psid}.dat'
#判断oss上是否存在 #判断oss上是否存在

2
libs/libs_db.py

@ -149,7 +149,7 @@ def isStudioConfigDistribute(psid):
try: try:
with pymysqlAlias() as conn: with pymysqlAlias() as conn:
cursor = conn.cursor() 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) cursor.execute(sql)
result = cursor.fetchone() result = cursor.fetchone()
if result[0] == 0: if result[0] == 0:

20
libs/main_service_db.py

@ -186,3 +186,23 @@ def update_task_distributed_detail(data):
print(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())} 执行update_task_distributed_detail({data})异常: {str(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)}") logging.error(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())} 执行update_task_distributed_detail({data})异常: {str(e)}")
return "error" 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'

47
logic/logic_main_service.py

@ -28,10 +28,10 @@ def get_task_distributed():
print("获取需要执行的步骤 next_step",next_step_result) print("获取需要执行的步骤 next_step",next_step_result)
return next_step_result return next_step_result
#非R11 R12 的主机在执行step2的时候,需要判断当前模型是否需要高精模或者photo3参与建模,如果是的话,该主机不执行这一步 #非R11 R12 的主机在执行step2的时候,需要判断当前模型是否需要高精模或者photo3参与建模,如果是的话,该主机不执行这一步
if next_step_result["run_step"] == "step2": # if next_step_result["run_step"] == "step2":
if common.task_need_high_model_or_photo3(next_step_result["task_key"]): # if common.task_need_high_model_or_photo3(next_step_result["task_key"]):
print(f'模型{next_step_result["task_key"]}需要高精模或者photo3参与建模,该主机{hostname}不执行step2') # print(f'模型{next_step_result["task_key"]}需要高精模或者photo3参与建模,该主机{hostname}不执行step2')
return "no" # return "no"
#taskData = next_step_result #{"hostname":hostname,"run_step":next_step,"task_distributed_id":result["id"],"task_key":result["task_key"]} #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) flagRes = update_main_and_add_detail(next_step_result)
if flagRes == "error": if flagRes == "error":
@ -42,24 +42,27 @@ def get_task_distributed():
else: else:
return 'no_data' return 'no_data'
else: else:
#R11 R12的主机 可以执行step1 2 3 的任务 #优先处理step1 和 step3
#如果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
#R11 R12的主机如果已经有在处理step2了,则不能再处理step2,只能处理step1 step3 #R11 R12的主机如果已经有在处理step2了,则不能再处理step2,只能处理step1 step3
resultData = need_run_step_no_step2() resultData = need_run_step_no_step2()
if resultData == "no": 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" return "no"
resultData["hostname"] = hostname resultData["hostname"] = hostname
flagRes = update_main_and_add_detail(resultData) flagRes = update_main_and_add_detail(resultData)
if flagRes == "error": if flagRes == "error":
@ -119,7 +122,7 @@ def need_run_stepx(task_distributed_id):
return "step3" return "step3"
elif result["step"] == "step3": elif result["step"] == "step3":
#这里要将 主任务表的状态改为2,finished_at改为当前时间 #这里要将 主任务表的状态改为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"
return "no" return "no"
except Exception as e: except Exception as e:
@ -180,7 +183,7 @@ def need_run_step_no_step2():
#判断是否有正在执行的step2 #判断是否有正在执行的step2
xstep = need_run_stepx(row["id"]) xstep = need_run_stepx(row["id"])
# print("查询非step2的任务列表",xstep) # print("查询非step2的任务列表",xstep)
if xstep != "step2" and (xstep == "step1" or xstep == "step3"): if xstep != "step2" and (xstep == "step1"):
#没有正在执行的step2,则返回该任务 #没有正在执行的step2,则返回该任务
return {"hostname":hostname,"run_step":xstep,"task_distributed_id":row["id"],"task_key":row["task_key"]} return {"hostname":hostname,"run_step":xstep,"task_distributed_id":row["id"],"task_key":row["task_key"]}
return "no" return "no"
@ -201,7 +204,9 @@ def need_run_step_no_step1():
#判断是否有正在执行的step2 #判断是否有正在执行的step2
xstep = need_run_stepx(row["id"]) xstep = need_run_stepx(row["id"])
#print("查询非step1的任务列表",xstep,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,则返回该任务 #没有正在执行的step1,则返回该任务
return {"hostname":hostname,"run_step":xstep,"task_distributed_id":row["id"],"task_key":row["task_key"]} return {"hostname":hostname,"run_step":xstep,"task_distributed_id":row["id"],"task_key":row["task_key"]}
return "no" return "no"

1
main_step1.py

@ -109,7 +109,6 @@ def cal_reconstruction_region(psid, pid):
print(cmd) print(cmd)
cmd = shlex.split(cmd) cmd = shlex.split(cmd)
res = subprocess.run(cmd) res = subprocess.run(cmd)
fix_region() fix_region()
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 重建区域计算完成') print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 重建区域计算完成')

32
main_step2.py

@ -24,6 +24,10 @@ def make3d(pid):
if get_rcver() == 1: # old version if get_rcver() == 1: # old version
#修改重建区域的大小 #修改重建区域的大小
common.change_rcbox_s(pid,"0.999") 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) simplify_value = 1000000 * libs.getHeadCount(pid)
add_photo3 = ' ' add_photo3 = ' '
@ -112,21 +116,6 @@ def make3d(pid):
cmd = shlex.split(cmd) cmd = shlex.split(cmd)
res = subprocess.run(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 else: # new version
@ -135,6 +124,11 @@ def make3d(pid):
calulate_type = 'calculateHighModel' calulate_type = 'calculateHighModel'
else: else:
calulate_type = 'calculateNormalModel' 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} \ cmd = f'{config.rcbin} {config.r2["init"]} -setInstanceName {pid} \
-load "{os.path.join(config.workdir, pid, f"{pid}.rcproj")}" \ -load "{os.path.join(config.workdir, pid, f"{pid}.rcproj")}" \
-{calulate_type} \ -{calulate_type} \
@ -145,6 +139,14 @@ def make3d(pid):
cmd = shlex.split(cmd) cmd = shlex.split(cmd)
res = subprocess.run(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=""): def step2(pid,task_distributed_id=""):
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 开始建模任务step2') print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} pid: {pid} 开始建模任务step2')

3
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
)

2
tools/auto_distance.py

@ -53,7 +53,7 @@ def get_defineDistances(pid, left, top, right, bottom):
psid = libs.getPSid(pid) psid = libs.getPSid(pid)
distances = libs_db.get_floor_sticker_distances(psid) #config.ps_floor_sticker.get(psid, config.ps_floor_sticker['default']) distances = libs_db.get_floor_sticker_distances(psid) #config.ps_floor_sticker.get(psid, config.ps_floor_sticker['default'])
time.sleep(5) time.sleep(5)
y = 748 y = 951
yCreateDistance = 0 yCreateDistance = 0
if len(distances.split(';')) == 1: if len(distances.split(';')) == 1:
yCreateDistance = 2 yCreateDistance = 2

10
tools/gen_xmps.py

@ -9,13 +9,17 @@ import config, libs, libs_db
def upload_xmp(pid): def upload_xmp(pid):
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 上传xmp文件之前先删除oss上的xmp文件所在目录...') print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 上传xmp文件之前先删除oss上的xmp文件所在目录...')
pid = str(pid)
psid = libs.getPSid(pid) psid = libs.getPSid(pid)
#移除掉旧的文件夹 #移除掉旧的文件夹
#config.oss_bucket.delete_object(f'xmps/{pid}/') #config.oss_bucket.delete_object(f'xmps/{pid}/')
#删除oss 上的文件夹里的内容 #删除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() start_time = time.time()
workdir = os.path.join(config.workdir, pid) workdir = os.path.join(config.workdir, pid)

Loading…
Cancel
Save