Skip to content

Commit 3e46d5c

Browse files
Add initial camera solver Python tool.
1 parent b0e65c1 commit 3e46d5c

File tree

3 files changed

+329
-0
lines changed

3 files changed

+329
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright (C) 2026 David Cattermole.
2+
#
3+
# This file is part of mmSolver.
4+
#
5+
# mmSolver is free software: you can redistribute it and/or modify it
6+
# under the terms of the GNU Lesser General Public License as
7+
# published by the Free Software Foundation, either version 3 of the
8+
# License, or (at your option) any later version.
9+
#
10+
# mmSolver is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with mmSolver. If not, see <https://www.gnu.org/licenses/>.
17+
#
18+
"""
19+
The camera solver tool and UI.
20+
"""
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (C) 2026 David Cattermole.
2+
#
3+
# This file is part of mmSolver.
4+
#
5+
# mmSolver is free software: you can redistribute it and/or modify it
6+
# under the terms of the GNU Lesser General Public License as
7+
# published by the Free Software Foundation, either version 3 of the
8+
# License, or (at your option) any later version.
9+
#
10+
# mmSolver is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with mmSolver. If not, see <https://www.gnu.org/licenses/>.
17+
#
18+
"""
19+
Holds all constant data needed for the camera solver tool and UI.
20+
"""
21+
22+
# Window Title Bar format.
23+
WINDOW_TITLE_BAR = 'Camera Solver'
24+
25+
# Window button text.
26+
WINDOW_BUTTON_SOLVE_START_LABEL = 'Solve'
27+
WINDOW_BUTTON_SOLVE_STOP_LABEL = 'Stop Solve'
28+
WINDOW_BUTTON_CLOSE_LABEL = 'Close'
29+
WINDOW_BUTTON_CLOSE_AND_STOP_LABEL = 'Stop Solve and Close'
30+
31+
# Available log levels for the Camera Solver UI.
32+
LOG_LEVEL_ERROR = 'error'
33+
LOG_LEVEL_WARNING = 'warning'
34+
LOG_LEVEL_PROGRESS = 'progress'
35+
LOG_LEVEL_INFO = 'info'
36+
LOG_LEVEL_DEBUG = 'debug'
37+
LOG_LEVEL_LIST = [
38+
LOG_LEVEL_ERROR,
39+
LOG_LEVEL_WARNING,
40+
LOG_LEVEL_PROGRESS,
41+
LOG_LEVEL_INFO,
42+
LOG_LEVEL_DEBUG,
43+
]
44+
45+
46+
# Adjustment solver type.
47+
ADJUSTMENT_SOLVER_TYPE_NONE = 'adjustment_solver_type_none'
48+
ADJUSTMENT_SOLVER_TYPE_EVOLUTION_REFINE = 'adjustment_solver_type_evolution_refine'
49+
ADJUSTMENT_SOLVER_TYPE_EVOLUTION_UNKNOWN = 'adjustment_solver_type_evolution_unknown'
50+
ADJUSTMENT_SOLVER_TYPE_UNIFORM_GRID = 'adjustment_solver_type_uniform_grid'
51+
ADJUSTMENT_SOLVER_TYPE_LIST = [
52+
ADJUSTMENT_SOLVER_TYPE_NONE,
53+
ADJUSTMENT_SOLVER_TYPE_EVOLUTION_REFINE,
54+
ADJUSTMENT_SOLVER_TYPE_EVOLUTION_UNKNOWN,
55+
ADJUSTMENT_SOLVER_TYPE_UNIFORM_GRID,
56+
]
57+
58+
# The environment variable name that is used to find the executable.
59+
MMSOLVER_LOCATION_ENV_VAR_NAME = 'MMSOLVER_LOCATION'
60+
61+
# The expected file name for the camera solver executable.
62+
EXECUTABLE_FILE_NAME = 'mmsolver-camerasolve'
63+
64+
65+
# This is a special attribute name that is expected by the
66+
# mmcamerasolve executable.
67+
ATTR_CAMERA_FOCAL_LENGTH = 'camera.focal_length_mm'
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
# Copyright (C) 2026 David Cattermole.
2+
#
3+
# This file is part of mmSolver.
4+
#
5+
# mmSolver is free software: you can redistribute it and/or modify it
6+
# under the terms of the GNU Lesser General Public License as
7+
# published by the Free Software Foundation, either version 3 of the
8+
# License, or (at your option) any later version.
9+
#
10+
# mmSolver is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with mmSolver. If not, see <https://www.gnu.org/licenses/>.
17+
#
18+
"""
19+
Library functions for Camera Solver.
20+
"""
21+
22+
import os
23+
import subprocess
24+
25+
import mmSolver.logger
26+
import mmSolver.api as mmapi
27+
28+
import mmSolver.utils.time as time_utils
29+
import mmSolver.utils.python_compat as pycompat
30+
31+
import mmSolver.tools.savemarkerfile.lib as savemarkerfile_lib
32+
import mmSolver.tools.savelensfile.lib as savelensfile_lib
33+
34+
import mmSolver.tools.camerasolver.constant as const
35+
36+
37+
LOG = mmSolver.logger.get_logger()
38+
39+
40+
class AdjustmentSolver(object):
41+
def __init__(self):
42+
# TODO: Should the class have default values?
43+
self.__adjustment_solver_type = None
44+
self.__thread_count = None
45+
self.__evolution_value_range_estimate = None
46+
self.__evolution_generation_count = None
47+
self.__evolution_population_count = None
48+
49+
def get_adjustment_solver_type(self):
50+
# type: (AdjustmentSolver) -> str | None
51+
return self.__adjustment_solver_type
52+
53+
def set_adjustment_solver_type(self, value):
54+
assert value in const.ADJUSTMENT_SOLVER_TYPE_LIST
55+
self.__adjustment_solver_type = value
56+
57+
def get_thread_count(self):
58+
# type: (AdjustmentSolver) -> int | None
59+
return self.__thread_count
60+
61+
def set_thread_count(self, value):
62+
assert isinstance(value, int)
63+
assert value > 0
64+
self.__thread_count = value
65+
66+
def get_evolution_value_range_estimate(self):
67+
return self.__evolution_value_range_estimate
68+
69+
def set_evolution_value_range_estimate(self, value):
70+
assert isinstance(value, int)
71+
assert value > 0
72+
self.__evolution_value_range_estimate = value
73+
74+
def get_evolution_generation_count(self):
75+
return self.__evolution_generation_count
76+
77+
def set_evolution_generation_count(self, value):
78+
assert isinstance(value, int)
79+
assert value > 0
80+
self.__evolution_generation_count = value
81+
82+
def get_evolution_population_count(self):
83+
# type: (AdjustmentSolver) -> int | None
84+
return self.__evolution_population_count
85+
86+
def set_evolution_population_count(self, value):
87+
assert isinstance(value, int)
88+
assert value > 0
89+
self.__evolution_population_count = value
90+
91+
92+
class AdjustmentAttributes(object):
93+
def __init__(self):
94+
# adjustment_attributes
95+
# - name ('camera.focal_length_mm')
96+
# - value_min
97+
# - value_max
98+
# - sample_count
99+
self.__attribute_to_bounds = {}
100+
self.__attribute_to_sample_count = {}
101+
102+
def get_attribute_bounds(self, attr_name):
103+
assert isinstance(attr_name, pycompat.TEXT_TYPE)
104+
return self.__attribute_to_bounds.get(attr_name)
105+
106+
def set_attribute_bounds(self, attr_name, min_value, max_value):
107+
assert isinstance(attr_name, pycompat.TEXT_TYPE)
108+
assert isinstance(min_value, float)
109+
assert isinstance(max_value, float)
110+
self.__attribute_to_bounds[attr_name] = [min_value, max_value]
111+
112+
def get_attribute_sample_count(self, attr_name):
113+
assert isinstance(attr_name, pycompat.TEXT_TYPE)
114+
return self.__attribute_to_sample_count.get(attr_name)
115+
116+
def set_attribute_sample_count(self, attr_name, value):
117+
assert isinstance(attr_name, pycompat.TEXT_TYPE)
118+
assert isinstance(value, int)
119+
assert value > 0
120+
self.__attribute_to_sample_count[attr_name] = value
121+
122+
123+
def construct_output_file_path(output_dir, file_prefix, file_suffix, file_ext):
124+
assert isinstance(output_dir, pycompat.TEXT_TYPE)
125+
assert isinstance(file_prefix, pycompat.TEXT_TYPE)
126+
assert isinstance(file_suffix, pycompat.TEXT_TYPE)
127+
assert isinstance(file_ext, pycompat.TEXT_TYPE)
128+
file_name = file_prefix + file_suffix + file_ext
129+
return os.path.join(output_dir, file_name)
130+
131+
132+
def save_markers_to_file(mkr_list, frame_range, file_prefix, output_dir):
133+
"""
134+
Save markers to disk.
135+
"""
136+
data = savemarkerfile_lib.generate(mkr_list, frame_range)
137+
file_suffix = ''
138+
file_path = construct_output_file_path(output_dir, file_prefix, file_suffix, '.uv')
139+
savemarkerfile_lib.write_file(file_path, data)
140+
return file_path
141+
142+
143+
def save_camera_to_file(cam, file_prefix, output_dir):
144+
"""
145+
Save camera to disk.
146+
"""
147+
# TODO: Implement this.
148+
file_suffix = ''
149+
file_path = construct_output_file_path(
150+
output_dir, file_prefix, file_suffix, '.mm camera'
151+
)
152+
return file_path
153+
154+
155+
def save_nuke_lens_to_file(cam, lens, frame_range, file_prefix, output_dir):
156+
"""
157+
Save nuke lens to disk.
158+
"""
159+
file_suffix = ''
160+
data_list = savelensfile_lib.generate(cam, lens, frame_range)
161+
file_path = construct_output_file_path(output_dir, file_prefix, file_suffix, '.nk')
162+
savelensfile_lib.write_nuke_file(file_path, data_list)
163+
return file_path
164+
165+
166+
def save_solver_settings_to_file(cam, file_prefix, output_dir):
167+
"""
168+
Save Solver settings to disk.
169+
"""
170+
# TODO: Implement this.
171+
file_suffix = ''
172+
file_path = construct_output_file_path(
173+
output_dir, file_prefix, file_suffix, '.mmsettings'
174+
)
175+
return file_path
176+
177+
178+
def __find_executable_file_path():
179+
"""
180+
Find the EXECUTABLE_FILE_NAME from the module directory.
181+
"""
182+
# type: (...) -> str | None
183+
var_name = const.MMSOLVER_LOCATION_ENV_VAR_NAME
184+
module_location = os.environ[var_name] # type: str
185+
assert os.path.isdir(module_location)
186+
executable_file_path = os.path.join(
187+
module_location, 'bin', const.EXECUTABLE_FILE_NAME
188+
)
189+
if not os.path.isfile(executable_file_path):
190+
return
191+
return executable_file_path
192+
193+
194+
def launch_solve(
195+
cam, # type: mmapi.Camera
196+
lens, # type: mmapi.Lens
197+
mkr_list, # type: list[mmapi.Marker]
198+
frame_range, # type: time_utils.FrameRange
199+
adjustment_solver, # type: AdjustmentSolver
200+
adjustment_attrs, # type: AdjustmentAttributes
201+
log_level, # type: str
202+
prefix_name, # type: str
203+
output_dir, # type: str
204+
):
205+
# type: (...) -> None
206+
"""
207+
Launch solver executable.
208+
209+
TODO: Read stdout from the process.
210+
211+
TODO: Do not block Maya main thread.
212+
"""
213+
assert isinstance(cam, mmapi.Camera)
214+
assert isinstance(lens, mmapi.Lens)
215+
assert isinstance(mkr_list, list)
216+
assert isinstance(frame_range, time_utils.FrameRange)
217+
assert isinstance(adjustment_solver, AdjustmentSolver)
218+
assert isinstance(adjustment_attrs, AdjustmentAttributes)
219+
assert log_level in const.LOG_LEVEL_LIST
220+
assert isinstance(prefix_name, pycompat.TEXT_TYPE)
221+
assert output_dir and os.path.isdir(output_dir)
222+
223+
executable_file_path = __find_executable_file_path()
224+
if executable_file_path is None:
225+
LOG.error('Could not find %r executable!', const.EXECUTABLE_FILE_NAME)
226+
return
227+
assert os.path.isfile(executable_file_path)
228+
229+
# TODO: Convert the arguments into command flags.
230+
cmd_args = [executable_file_path, '--help']
231+
subprocess.call(cmd_args)
232+
return
233+
234+
235+
def load_camera_outputs():
236+
raise NotImplementedError
237+
# TODO: Read camera outputs
238+
239+
240+
def load_nuke_lens_file():
241+
# TODO: Read lens file.
242+
raise NotImplementedError

0 commit comments

Comments
 (0)