obj_trimmer.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. # Copyright (c) 2022 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import argparse
  4. import os
  5. from typing import Optional, List, TextIO
  6. """
  7. Used to reduce the size of obj files used for printer platform models.
  8. Trims trailing 0 from coordinates
  9. Removes duplicate vertex texture coordinates
  10. Removes any rows that are not a face, vertex or vertex texture
  11. """
  12. def process_obj(input_file: str, output_file: str) -> None:
  13. with open(input_file, "r") as in_obj, open("temp", "w") as temp:
  14. trim_lines(in_obj, temp)
  15. with open("temp", "r") as temp, open(output_file, "w") as out_obj:
  16. merge_duplicate_vt(temp, out_obj)
  17. os.remove("temp")
  18. def trim_lines(in_obj: TextIO, out_obj: TextIO) -> None:
  19. for line in in_obj:
  20. line = trim_line(line)
  21. if line:
  22. out_obj.write(line + "\n")
  23. def trim_line(line: str) -> Optional[str]:
  24. # Discards all rows that are not a vertex ("v"), face ("f") or vertex texture ("vt")
  25. values = line.split()
  26. if values[0] == "vt":
  27. return trim_vertex_texture(values)
  28. elif values[0] == "f":
  29. return trim_face(values)
  30. elif values[0] == "v":
  31. return trim_vertex(values)
  32. return
  33. def trim_face(values: List[str]) -> str:
  34. # Removes face normals (vn)
  35. # f 15/15/17 15/15/17 14/14/17 -> f 15/15 15/15 14/14
  36. for i, coordinates in enumerate(values[1:]):
  37. v, vt = coordinates.split("/")[:2]
  38. values[i + 1] = v + "/" + vt
  39. return " ".join(values)
  40. def trim_vertex(values: List[str]) -> str:
  41. # Removes trailing zeros from vertex coordinates
  42. # v 0.044000 0.137000 0.123000 -> v 0.044 0.137 0.123
  43. for i, coordinate in enumerate(values[1:]):
  44. values[i + 1] = str(float(coordinate))
  45. return " ".join(values)
  46. def trim_vertex_texture(values: List[str]) -> str:
  47. # Removes trailing zeros from vertex texture coordinates
  48. # vt 0.137000 0.123000 -> v 0.137 0.123
  49. for i, coordinate in enumerate(values[1:]):
  50. values[i + 1] = str(float(coordinate))
  51. return " ".join(values)
  52. def merge_duplicate_vt(in_obj, out_obj):
  53. # Removes duplicate vertex texture ("vt")
  54. # Points references to all deleted copies in face ("f") to a single vertex texture
  55. # Maps index of all copies of a vt line to the same index
  56. vt_index_mapping = {}
  57. # Maps vt line to index ("vt 0.043 0.137" -> 23)
  58. vt_to_index = {}
  59. # .obj file indexes start at 1
  60. vt_index = 1
  61. skipped_count = 0
  62. # First write everything except faces
  63. for line in in_obj.readlines():
  64. if line[0] == "f":
  65. continue
  66. if line[:2] == "vt":
  67. if line in vt_to_index.keys():
  68. # vt with same value has already been written
  69. # this points the current vt index to the one that has been written
  70. vt_index_mapping[vt_index] = vt_to_index[line]
  71. skipped_count += 1
  72. else:
  73. # vt has not been seen, point vt line to index
  74. vt_to_index[line] = vt_index - skipped_count
  75. vt_index_mapping[vt_index] = vt_index - skipped_count
  76. out_obj.write(line)
  77. vt_index += 1
  78. else:
  79. out_obj.write(line)
  80. # Second pass remaps face vt index
  81. in_obj.seek(0)
  82. for line in in_obj.readlines():
  83. if line[0] != "f":
  84. continue
  85. values = line.split()
  86. for i, coordinates in enumerate(values[1:]):
  87. v, vt = coordinates.split("/")[:2]
  88. vt = int(vt)
  89. if vt in vt_index_mapping.keys():
  90. vt = vt_index_mapping[vt]
  91. values[i + 1] = v + "/" + str(vt)
  92. out_obj.write(" ".join(values) + "\n")
  93. if __name__ == "__main__":
  94. parser = argparse.ArgumentParser(description = "Reduce the size of a .obj file")
  95. parser.add_argument("input_file", type = str, help = "Input .obj file name")
  96. parser.add_argument("--output_file", default = "output.obj", type = str, help = "Output .obj file name")
  97. args = parser.parse_args()
  98. process_obj(args.input_file, args.output_file)