Coverage for trimesh/scene/cameras.py: 92%

131 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1import copy 

2 

3import numpy as np 

4 

5from .. import util 

6 

7 

8class Camera: 

9 def __init__( 

10 self, name=None, resolution=None, focal=None, fov=None, z_near=0.01, z_far=1000.0 

11 ): 

12 """ 

13 Create a new Camera object that stores camera intrinsic 

14 and extrinsic parameters. 

15 

16 TODO: skew is not supported 

17 TODO: cx and cy that are not half of width and height 

18 

19 Parameters 

20 ------------ 

21 name : str or None 

22 Name for camera to be used as node name 

23 resolution : (2,) int 

24 Pixel size in (height, width) 

25 focal : (2,) float 

26 Focal length in pixels. Either pass this OR FOV 

27 but not both. focal = (K[0][0], K[1][1]) 

28 fov : (2,) float 

29 Field of view (fovx, fovy) in degrees 

30 z_near : float 

31 What is the closest 

32 """ 

33 

34 if name is None: 

35 # if name is not passed, make it something unique 

36 self.name = f"camera_{util.unique_id(6).upper()}" 

37 else: 

38 # otherwise assign it 

39 self.name = name 

40 

41 if fov is None and focal is None: 

42 raise ValueError("either focal length or FOV required!") 

43 

44 # store whether or not we computed the focal length 

45 self._focal_computed = False 

46 

47 # set the passed (2,) float focal length 

48 self.focal = focal 

49 

50 # set the passed (2,) float FOV in degrees 

51 self.fov = fov 

52 

53 if resolution is None: 

54 # if unset make resolution 30 pixels per degree 

55 resolution = (self.fov * 30.0).round().astype(np.int64) 

56 self.resolution = resolution 

57 

58 # what is the farthest from the camera it should render 

59 self.z_far = float(z_far) 

60 # what is the closest to the camera it should render 

61 self.z_near = float(z_near) 

62 

63 def copy(self): 

64 """ 

65 Safely get a copy of the current camera. 

66 """ 

67 return Camera( 

68 name=copy.deepcopy(self.name), 

69 resolution=copy.deepcopy(self.resolution), 

70 focal=copy.deepcopy(self.focal), 

71 fov=copy.deepcopy(self.fov), 

72 ) 

73 

74 @property 

75 def resolution(self): 

76 """ 

77 Get the camera resolution in pixels. 

78 

79 Returns 

80 ------------ 

81 resolution (2,) float 

82 Camera resolution in pixels 

83 """ 

84 return self._resolution 

85 

86 @resolution.setter 

87 def resolution(self, values): 

88 """ 

89 Set the camera resolution in pixels. 

90 

91 Parameters 

92 ------------ 

93 resolution (2,) float 

94 Camera resolution in pixels 

95 """ 

96 values = np.asanyarray(values, dtype=np.int64) 

97 if values.shape != (2,): 

98 raise ValueError("resolution must be (2,) float") 

99 values.flags.writeable = False 

100 self._resolution = values 

101 # unset computed value that depends on the other plus resolution 

102 if self._focal_computed: 

103 self._focal = None 

104 else: 

105 # fov must be computed 

106 self._fov = None 

107 

108 @property 

109 def focal(self): 

110 """ 

111 Get the focal length in pixels for the camera. 

112 

113 Returns 

114 ------------ 

115 focal : (2,) float 

116 Focal length in pixels 

117 """ 

118 if self._focal is None: 

119 # calculate focal length from FOV 

120 focal = self._resolution / (2.0 * np.tan(np.radians(self._fov / 2.0))) 

121 focal.flags.writeable = False 

122 self._focal = focal 

123 

124 return self._focal 

125 

126 @focal.setter 

127 def focal(self, values): 

128 """ 

129 Set the focal length in pixels for the camera. 

130 

131 Returns 

132 ------------ 

133 focal : (2,) float 

134 Focal length in pixels. 

135 """ 

136 if values is None: 

137 self._focal = None 

138 else: 

139 # flag this as not computed (hence fov must be) 

140 # this is necessary so changes to resolution can reset the 

141 # computed quantity without changing the explicitly set quantity 

142 self._focal_computed = False 

143 values = np.asanyarray(values, dtype=np.float64) 

144 if values.shape != (2,): 

145 raise ValueError("focal length must be (2,) float") 

146 values.flags.writeable = False 

147 # assign passed values to focal length 

148 self._focal = values 

149 # focal overrides FOV 

150 self._fov = None 

151 

152 @property 

153 def K(self): 

154 """ 

155 Get the intrinsic matrix for the Camera object. 

156 

157 Returns 

158 ----------- 

159 K : (3, 3) float 

160 Intrinsic matrix for camera 

161 """ 

162 K = np.eye(3, dtype=np.float64) 

163 K[0, 0] = self.focal[0] 

164 K[1, 1] = self.focal[1] 

165 K[:2, 2] = self.resolution / 2.0 

166 return K 

167 

168 @K.setter 

169 def K(self, values): 

170 if values is None: 

171 return 

172 values = np.asanyarray(values, dtype=np.float64) 

173 if values.shape != (3, 3): 

174 raise ValueError("matrix must be (3,3)!") 

175 

176 if not np.allclose(values.flatten()[[1, 3, 6, 7, 8]], [0, 0, 0, 0, 1]): 

177 raise ValueError("matrix should only have focal length and resolution!") 

178 

179 # set focal length from matrix 

180 self.focal = [values[0, 0], values[1, 1]] 

181 # set resolution from matrix 

182 self.resolution = values[:2, 2] * 2 

183 

184 @property 

185 def fov(self): 

186 """ 

187 Get the field of view in degrees. 

188 

189 Returns 

190 ------------- 

191 fov : (2,) float 

192 XY field of view in degrees 

193 """ 

194 if self._fov is None: 

195 fov = 2.0 * np.degrees(np.arctan((self._resolution / 2.0) / self._focal)) 

196 fov.flags.writeable = False 

197 self._fov = fov 

198 return self._fov 

199 

200 @fov.setter 

201 def fov(self, values): 

202 """ 

203 Set the field of view in degrees. 

204 

205 Parameters 

206 ------------- 

207 values : (2,) float 

208 Size of FOV to set in degrees 

209 """ 

210 if values is None: 

211 self._fov = None 

212 else: 

213 # flag this as computed (hence fov must not be) 

214 # this is necessary so changes to resolution can reset the 

215 # computed quantity without changing the explicitly set quantity 

216 self._focal_computed = True 

217 values = np.asanyarray(values, dtype=np.float64) 

218 if values.shape != (2,): 

219 raise ValueError("fov length must be (2,) int") 

220 values.flags.writeable = False 

221 # assign passed values to FOV 

222 self._fov = values 

223 # fov overrides focal 

224 self._focal = None 

225 

226 def to_rays(self): 

227 """ 

228 Calculate ray direction vectors. 

229 

230 Will return one ray per pixel, as set in self.resolution. 

231 

232 Returns 

233 -------------- 

234 vectors : (n, 3) float 

235 Ray direction vectors in camera frame with z == -1 

236 """ 

237 return camera_to_rays(self) 

238 

239 def angles(self): 

240 """ 

241 Get ray spherical coordinates in radians. 

242 

243 

244 Returns 

245 -------------- 

246 angles : (n, 2) float 

247 Ray spherical coordinate angles in radians. 

248 """ 

249 return np.arctan(-ray_pixel_coords(self)) 

250 

251 def look_at(self, points, **kwargs): 

252 """ 

253 Generate transform for a camera to keep a list 

254 of points in the camera's field of view. 

255 

256 Parameters 

257 ------------- 

258 points : (n, 3) float 

259 Points in space 

260 rotation : None, or (4, 4) float 

261 Rotation matrix for initial rotation 

262 distance : None or float 

263 Distance from camera to center 

264 center : None, or (3,) float 

265 Center of field of view. 

266 

267 Returns 

268 -------------- 

269 transform : (4, 4) float 

270 Transformation matrix from world to camera 

271 """ 

272 return look_at(points, fov=self.fov, **kwargs) 

273 

274 def __repr__(self): 

275 return f"<trimesh.scene.Camera> FOV: {self.fov} Resolution: {self.resolution}" 

276 

277 

278def look_at(points, fov, rotation=None, distance=None, center=None, pad=None): 

279 """ 

280 Generate transform for a camera to keep a list 

281 of points in the camera's field of view. 

282 

283 Examples 

284 ------------ 

285 ```python 

286 points = np.array([0, 0, 0], [1, 1, 1]) 

287 scene.camera_transform = scene.camera.look_at(points) 

288 ``` 

289 

290 Parameters 

291 ------------- 

292 points : (n, 3) float 

293 Points in space 

294 fov : (2,) float 

295 Field of view, in DEGREES 

296 rotation : None, or (4, 4) float 

297 Rotation matrix for initial rotation 

298 distance : None or float 

299 Distance from camera to center 

300 center : None, or (3,) float 

301 Center of field of view. 

302 

303 Returns 

304 -------------- 

305 transform : (4, 4) float 

306 Transformation matrix from world to camera 

307 """ 

308 

309 if rotation is None: 

310 rotation = np.eye(4) 

311 else: 

312 rotation = np.asanyarray(rotation, dtype=np.float64) 

313 points = np.asanyarray(points, dtype=np.float64) 

314 

315 # Transform points to camera frame (just use the rotation part) 

316 rinv = rotation[:3, :3].T 

317 points_c = rinv.dot(points.T).T 

318 

319 if center is None: 

320 # Find the center of the points' AABB in camera frame 

321 center_c = points_c.min(axis=0) + 0.5 * np.ptp(points_c, axis=0) 

322 else: 

323 # Transform center to camera frame 

324 center_c = rinv.dot(center) 

325 

326 # Re-center the points around the camera-frame origin 

327 points_c -= center_c 

328 

329 # Find the minimum distance for the camera from the origin 

330 # so that all points fit in the view frustum 

331 tfov = np.tan(np.radians(fov) / 2.0) 

332 

333 if distance is None: 

334 distance = np.max(np.abs(points_c[:, :2]) / tfov + points_c[:, 2][:, np.newaxis]) 

335 

336 if pad is not None: 

337 distance *= pad 

338 

339 # set the pose translation 

340 center_w = rotation[:3, :3].dot(center_c) 

341 cam_pose = rotation.copy() 

342 cam_pose[:3, 3] = center_w + distance * cam_pose[:3, 2] 

343 

344 return cam_pose 

345 

346 

347def ray_pixel_coords(camera): 

348 """ 

349 Get the x-y coordinates of rays in camera coordinates at 

350 z == -1. 

351 

352 One coordinate pair will be given for each pixel as defined in 

353 camera.resolution. If reshaped, the returned array corresponds 

354 to pixels of the rendered image. 

355 

356 Examples 

357 ------------ 

358 ```python 

359 xy = ray_pixel_coords(camera).reshape( 

360 tuple(camera.coordinates) + (2,)) 

361 top_left == xy[0, 0] 

362 bottom_right == xy[-1, -1] 

363 ``` 

364 

365 Parameters 

366 -------------- 

367 camera : trimesh.scene.Camera 

368 Camera object to generate rays from 

369 

370 Returns 

371 -------------- 

372 xy : (n, 2) float 

373 x-y coordinates of intersection of each camera ray 

374 with the z == -1 frame 

375 """ 

376 # shorthand 

377 res = camera.resolution 

378 half_fov = np.radians(camera.fov) / 2.0 

379 

380 right_top = np.tan(half_fov) 

381 # move half a pixel width in 

382 right_top *= 1 - (1.0 / res) 

383 left_bottom = -right_top 

384 # we are looking down the negative z axis, so 

385 # right_top corresponds to maximum x/y values 

386 # bottom_left corresponds to minimum x/y values 

387 right, top = right_top 

388 left, bottom = left_bottom 

389 

390 # create a grid of vectors 

391 xy = util.grid_linspace( 

392 bounds=[[left, top], [right, bottom]], count=camera.resolution 

393 ) 

394 

395 # create a matching array of pixel indexes for the rays 

396 pixels = util.grid_linspace( 

397 bounds=[[0, res[1] - 1], [res[0] - 1, 0]], count=res 

398 ).astype(np.int64) 

399 assert xy.shape == pixels.shape 

400 

401 return xy, pixels 

402 

403 

404def camera_to_rays(camera: Camera): 

405 """ 

406 Calculate the trimesh.scene.Camera object to direction vectors. 

407 

408 Will return one ray per pixel, as set in camera.resolution. 

409 

410 Parameters 

411 -------------- 

412 camera : trimesh.scene.Camera 

413 

414 Returns 

415 -------------- 

416 vectors : (n, 3) float 

417 Ray direction vectors in camera frame with z == -1 

418 """ 

419 # get the on-plane coordinates 

420 xy, pixels = ray_pixel_coords(camera) 

421 # convert vectors to 3D unit vectors 

422 vectors = util.unitize(np.column_stack((xy, -np.ones_like(xy[:, :1])))) 

423 return vectors, pixels