1919Library functions for Camera Solver.
2020"""
2121
22+ import json
2223import os
2324import subprocess
25+ import threading
26+
2427
2528import mmSolver .logger
2629import mmSolver .api as mmapi
27-
2830import mmSolver .utils .time as time_utils
2931import mmSolver .utils .python_compat as pycompat
30-
3132import mmSolver .tools .savemarkerfile .lib as savemarkerfile_lib
3233import mmSolver .tools .savelensfile .lib as savelensfile_lib
3334
35+ import maya .cmds
36+
3437import mmSolver .tools .camerasolver .constant as const
3538
3639
3740LOG = mmSolver .logger .get_logger ()
3841
42+ # Maya stores film aperture in inches camera solver expects mm.
43+ INCHES_TO_MM = 25.4
44+
3945
4046class AdjustmentSolver (object ):
4147 def __init__ (self ):
42- # TODO: Should the class have default values?
4348 self .__adjustment_solver_type = None
4449 self .__thread_count = None
4550 self .__evolution_value_range_estimate = None
@@ -91,14 +96,13 @@ def set_evolution_population_count(self, value):
9196
9297class AdjustmentAttributes (object ):
9398 def __init__ (self ):
94- # adjustment_attributes
95- # - name ('camera.focal_length_mm')
96- # - value_min
97- # - value_max
98- # - sample_count
9999 self .__attribute_to_bounds = {}
100100 self .__attribute_to_sample_count = {}
101101
102+ def get_attribute_names (self ):
103+ # type: (AdjustmentAttributes) -> list[str]
104+ return list (self .__attribute_to_bounds .keys ())
105+
102106 def get_attribute_bounds (self , attr_name ):
103107 assert isinstance (attr_name , pycompat .TEXT_TYPE )
104108 return self .__attribute_to_bounds .get (attr_name )
@@ -130,70 +134,184 @@ def construct_output_file_path(output_dir, file_prefix, file_suffix, file_ext):
130134
131135
132136def save_markers_to_file (mkr_list , frame_range , file_prefix , output_dir ):
133- """
134- Save markers to disk.
135- """
136137 data = savemarkerfile_lib .generate (mkr_list , frame_range )
137138 file_suffix = ''
138139 file_path = construct_output_file_path (output_dir , file_prefix , file_suffix , '.uv' )
139140 savemarkerfile_lib .write_file (file_path , data )
140141 return file_path
141142
142143
143- def save_camera_to_file (cam , file_prefix , output_dir ):
144- """
145- Save camera to disk.
146- """
147- # TODO: Implement this.
144+ def _query_sample_attr_over_frames (node , attr_name , frames ):
145+ result = []
146+ for frame in frames :
147+ value = maya .cmds .getAttr (node + '.' + attr_name , time = frame )
148+ result .append ([frame , value ])
149+ return result
150+
151+
152+ def save_camera_to_file (cam , frame_range , file_prefix , output_dir ):
153+ assert isinstance (cam , mmapi .Camera )
154+ assert isinstance (frame_range , time_utils .FrameRange )
148155 file_suffix = ''
149156 file_path = construct_output_file_path (
150- output_dir , file_prefix , file_suffix , '.mm camera '
157+ output_dir , file_prefix , file_suffix , '.mmcamera '
151158 )
159+
160+ cam_tfm = cam .get_transform_node ()
161+ cam_shp = cam .get_shape_node ()
162+ assert cam_tfm is not None
163+ assert cam_shp is not None
164+
165+ frames = list (range (frame_range .start , frame_range .end + 1 ))
166+
167+ def _sample_mm (node , attr ):
168+ raw = _query_sample_attr_over_frames (node , attr , frames )
169+ return [[f , v * INCHES_TO_MM ] for f , v in raw ]
170+
171+ attr_data = {
172+ 'translateX' : _query_sample_attr_over_frames (cam_tfm , 'translateX' , frames ),
173+ 'translateY' : _query_sample_attr_over_frames (cam_tfm , 'translateY' , frames ),
174+ 'translateZ' : _query_sample_attr_over_frames (cam_tfm , 'translateZ' , frames ),
175+ 'rotateX' : _query_sample_attr_over_frames (cam_tfm , 'rotateX' , frames ),
176+ 'rotateY' : _query_sample_attr_over_frames (cam_tfm , 'rotateY' , frames ),
177+ 'rotateZ' : _query_sample_attr_over_frames (cam_tfm , 'rotateZ' , frames ),
178+ 'focalLength' : _query_sample_attr_over_frames (cam_shp , 'focalLength' , frames ),
179+ 'filmBackWidth' : _sample_mm (cam_shp , 'horizontalFilmAperture' ),
180+ 'filmBackHeight' : _sample_mm (cam_shp , 'verticalFilmAperture' ),
181+ 'filmBackOffsetX' : _sample_mm (cam_shp , 'horizontalFilmOffset' ),
182+ 'filmBackOffsetY' : _sample_mm (cam_shp , 'verticalFilmOffset' ),
183+ }
184+
185+ image_width , image_height = cam .get_plate_resolution ()
186+ pixel_aspect = None
187+
188+ cam_name = cam_tfm .split ('|' )[- 1 ]
189+
190+ doc = {
191+ 'version' : 1 ,
192+ 'data' : {
193+ 'name' : cam_name ,
194+ 'start_frame' : frame_range .start ,
195+ 'end_frame' : frame_range .end ,
196+ 'image' : {
197+ 'width' : image_width ,
198+ 'height' : image_height ,
199+ 'pixel_aspect_ratio' : pixel_aspect ,
200+ 'file_path' : None ,
201+ },
202+ 'attr' : attr_data ,
203+ },
204+ }
205+
206+ with open (file_path , 'w' ) as fh :
207+ json .dump (doc , fh )
152208 return file_path
153209
154210
155211def save_nuke_lens_to_file (cam , lens , frame_range , file_prefix , output_dir ):
156- """
157- Save nuke lens to disk.
158- """
159212 file_suffix = ''
160213 data_list = savelensfile_lib .generate (cam , lens , frame_range )
161214 file_path = construct_output_file_path (output_dir , file_prefix , file_suffix , '.nk' )
162215 savelensfile_lib .write_nuke_file (file_path , data_list )
163216 return file_path
164217
165218
166- def save_solver_settings_to_file (cam , file_prefix , output_dir ):
167- """
168- Save Solver settings to disk.
169- """
170- # TODO: Implement this.
219+ def save_solver_settings_to_file (
220+ frame_range ,
221+ adjustment_solver ,
222+ adjustment_attrs ,
223+ file_prefix ,
224+ output_dir ,
225+ ):
226+ assert isinstance (frame_range , time_utils .FrameRange )
227+ assert isinstance (adjustment_solver , AdjustmentSolver )
228+ assert isinstance (adjustment_attrs , AdjustmentAttributes )
229+
171230 file_suffix = ''
172231 file_path = construct_output_file_path (
173232 output_dir , file_prefix , file_suffix , '.mmsettings'
174233 )
234+
235+ solver_type_map = {
236+ const .ADJUSTMENT_SOLVER_TYPE_EVOLUTION_REFINE : 'evolution_refine' ,
237+ const .ADJUSTMENT_SOLVER_TYPE_EVOLUTION_UNKNOWN : 'evolution_unknown' ,
238+ const .ADJUSTMENT_SOLVER_TYPE_UNIFORM_GRID : 'uniform_grid' ,
239+ }
240+ adj_type = adjustment_solver .get_adjustment_solver_type ()
241+ adj_type_str = None
242+ if adj_type is not None :
243+ adj_type_str = solver_type_map .get (adj_type )
244+
245+ adj_solver_data = None
246+ if adj_type_str is not None :
247+ thread_count = adjustment_solver .get_thread_count ()
248+ value_range_estimate = adjustment_solver .get_evolution_value_range_estimate ()
249+ if value_range_estimate is None :
250+ value_range_estimate = True
251+
252+ gen_count = adjustment_solver .get_evolution_generation_count ()
253+ pop_count = adjustment_solver .get_evolution_population_count ()
254+ adj_solver_data = {
255+ 'type' : adj_type_str ,
256+ 'thread_count' : thread_count ,
257+ 'evolution_value_range_estimate' : value_range_estimate ,
258+ 'evolution_generation_count' : gen_count ,
259+ 'evolution_population_count' : pop_count ,
260+ }
261+
262+ attr_list = []
263+ for attr_name in adjustment_attrs .get_attribute_names ():
264+ bounds = adjustment_attrs .get_attribute_bounds (attr_name )
265+ sample_count = adjustment_attrs .get_attribute_sample_count (attr_name )
266+ if bounds :
267+ value_min = bounds [0 ]
268+ value_max = bounds [1 ]
269+ else :
270+ value_min = 0.0
271+ value_max = 200.0
272+ entry = {
273+ 'name' : attr_name ,
274+ 'value_min' : value_min ,
275+ 'value_max' : value_max ,
276+ 'sample_count' : sample_count ,
277+ }
278+ attr_list .append (entry )
279+
280+ data_section = {
281+ 'origin_frame' : frame_range .start ,
282+ 'frames' : {
283+ 'start_frame' : frame_range .start ,
284+ 'end_frame' : frame_range .end ,
285+ },
286+ 'adjustment_attributes' : attr_list ,
287+ }
288+ if adj_solver_data is not None :
289+ data_section ['adjustment_solver' ] = adj_solver_data
290+
291+ doc = {'version' : 1 , 'data' : data_section }
292+
293+ with open (file_path , 'w' ) as fh :
294+ json .dump (doc , fh , indent = 4 )
175295 return file_path
176296
177297
178- def __find_executable_file_path ():
179- """
180- Find the EXECUTABLE_FILE_NAME from the module directory.
181- """
298+ def find_executable_file_path ():
182299 # type: (...) -> str | None
183300 var_name = const .MMSOLVER_LOCATION_ENV_VAR_NAME
184- module_location = os .environ [var_name ] # type: str
185- assert os .path .isdir (module_location )
301+ module_location = os .environ .get (var_name )
302+ if not module_location or not os .path .isdir (module_location ):
303+ return None
186304 executable_file_path = os .path .join (
187305 module_location , 'bin' , const .EXECUTABLE_FILE_NAME
188306 )
189307 if not os .path .isfile (executable_file_path ):
190- return
308+ return None
191309 return executable_file_path
192310
193311
194312def launch_solve (
195313 cam , # type: mmapi.Camera
196- lens , # type: mmapi.Lens
314+ lens , # type: mmapi.Lens | None
197315 mkr_list , # type: list[mmapi.Marker]
198316 frame_range , # type: time_utils.FrameRange
199317 adjustment_solver , # type: AdjustmentSolver
@@ -202,16 +320,15 @@ def launch_solve(
202320 prefix_name , # type: str
203321 output_dir , # type: str
204322):
205- # type: (...) -> None
323+ # type: (...) -> tuple[int, str, str]
206324 """
207- Launch solver executable.
325+ Write .uv/.mmcamera/.mmsettings (and optionally .nk) then run the
326+ camera solver executable.
208327
209- TODO: Read stdout from the process.
210-
211- TODO: Do not block Maya main thread.
328+ Returns (returncode, stdout, stderr).
212329 """
213330 assert isinstance (cam , mmapi .Camera )
214- assert isinstance (lens , mmapi .Lens )
331+ assert lens is None or isinstance (lens , mmapi .Lens )
215332 assert isinstance (mkr_list , list )
216333 assert isinstance (frame_range , time_utils .FrameRange )
217334 assert isinstance (adjustment_solver , AdjustmentSolver )
@@ -220,16 +337,83 @@ def launch_solve(
220337 assert isinstance (prefix_name , pycompat .TEXT_TYPE )
221338 assert output_dir and os .path .isdir (output_dir )
222339
223- executable_file_path = __find_executable_file_path ()
340+ executable_file_path = find_executable_file_path ()
224341 if executable_file_path is None :
225342 LOG .error ('Could not find %r executable!' , const .EXECUTABLE_FILE_NAME )
226- return
227- assert os .path .isfile (executable_file_path )
343+ return (- 1 , '' , '' )
344+
345+ uv_file_path = save_markers_to_file (mkr_list , frame_range , prefix_name , output_dir )
346+ mmcamera_file_path = save_camera_to_file (cam , frame_range , prefix_name , output_dir )
347+ solver_settings_file_path = save_solver_settings_to_file (
348+ frame_range , adjustment_solver , adjustment_attrs , prefix_name , output_dir
349+ )
350+
351+ nuke_lens_file_path = None
352+ if lens is not None :
353+ nuke_lens_file_path = save_nuke_lens_to_file (
354+ cam , lens , frame_range , prefix_name , output_dir
355+ )
356+
357+ cmd_args = [
358+ executable_file_path ,
359+ uv_file_path ,
360+ '--mmcamera' ,
361+ mmcamera_file_path ,
362+ '--solver-settings' ,
363+ solver_settings_file_path ,
364+ ]
365+
366+ if nuke_lens_file_path is not None :
367+ cmd_args += ['--nuke-lens' , nuke_lens_file_path ]
368+
369+ cmd_args += [
370+ '--prefix' ,
371+ prefix_name ,
372+ '--output-dir' ,
373+ output_dir ,
374+ '--log-level' ,
375+ log_level ,
376+ ]
377+
378+ LOG .debug ('Camera solver command: %s' , ' ' .join (cmd_args ))
379+ proc = subprocess .Popen (
380+ cmd_args ,
381+ stdout = subprocess .PIPE ,
382+ stderr = subprocess .PIPE ,
383+ )
228384
229- # TODO: Convert the arguments into command flags.
230- cmd_args = [executable_file_path , '--help' ]
231- subprocess .call (cmd_args )
232- return
385+ stdout_lines = []
386+ stderr_lines = []
387+
388+ def _read_stream (stream , line_list , log_fn ):
389+ for raw in iter (stream .readline , b'' ):
390+ line = raw .decode ('utf-8' , errors = 'replace' ).rstrip ('\n ' )
391+ line_list .append (line )
392+ log_fn ('%s' , line )
393+ stream .close ()
394+
395+ stdout_thread = threading .Thread (
396+ target = _read_stream ,
397+ args = (proc .stdout , stdout_lines , LOG .info ),
398+ daemon = True ,
399+ )
400+ stderr_thread = threading .Thread (
401+ target = _read_stream ,
402+ args = (proc .stderr , stderr_lines , LOG .warning ),
403+ daemon = True ,
404+ )
405+ stdout_thread .start ()
406+ stderr_thread .start ()
407+ stdout_thread .join ()
408+ stderr_thread .join ()
409+ proc .wait ()
410+
411+ stdout = '\n ' .join (stdout_lines )
412+ stderr = '\n ' .join (stderr_lines )
413+ returncode = proc .returncode
414+ if returncode != 0 :
415+ LOG .error ('Camera solver failed (exit %d):\n %s' , returncode , stderr or stdout )
416+ return (returncode , stdout , stderr )
233417
234418
235419def load_camera_outputs ():
0 commit comments