Coverage for python / lsst / display / firefly / firefly.py: 11%

286 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-23 08:27 +0000

1# 

2# LSST Data Management System 

3# Copyright 2008, 2009, 2010, 2015 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23import logging 

24from io import BytesIO 

25from socket import gaierror 

26 

27import lsst.afw.display.interface as interface 

28import lsst.afw.display.virtualDevice as virtualDevice 

29import lsst.afw.display.ds9Regions as ds9Regions 

30import lsst.afw.display as afwDisplay 

31import lsst.afw.math as afwMath 

32 

33from .footprints import createFootprintsTable 

34 

35try: 

36 import firefly_client 

37 _fireflyClient = None 

38except ImportError as e: 

39 raise RuntimeError(f"Cannot import firefly_client: {e}") 

40from ws4py.client import HandshakeError 

41 

42_LOG = logging.getLogger(__name__) 

43 

44 

45class FireflyError(Exception): 

46 

47 def __init__(self, str): 

48 Exception.__init__(self, str) 

49 

50 

51def firefly_version(): 

52 """Return the version of firefly_client in use, as a string""" 

53 return firefly_client.__version__ 

54 

55 

56class DisplayImpl(virtualDevice.DisplayImpl): 

57 """Device to talk to a firefly display""" 

58 

59 @staticmethod 

60 def __handleCallbacks(event): 

61 if 'type' in event['data']: 

62 if event['data']['type'] == 'AREA_SELECT': 

63 _LOG.debug('*************area select') 

64 pParams = {'URL': 'http://web.ipac.caltech.edu/staff/roby/demo/wise-m51-band2.fits', 

65 'ColorTable': '9'} 

66 plot_id = 3 

67 _fireflyClient.show_fits_image(fileOnServer=None, plot_id=plot_id, additionalParams=pParams) 

68 

69 _LOG.debug("Callback event info: %s", event) 

70 return 

71 data = dict(_.split('=') for _ in event.get('data', {}).split('&')) 

72 if data.get('type') == "POINT": 

73 _LOG.debug("Event Received: %s", data.get('id')) 

74 

75 def __init__(self, display, verbose=False, url=None, 

76 name=None, *args, **kwargs): 

77 virtualDevice.DisplayImpl.__init__(self, display, verbose) 

78 

79 if self.verbose: 

80 print("Opening firefly device %s" % (self.display.frame if self.display else "[None]")) 

81 

82 global _fireflyClient 

83 if not _fireflyClient: 

84 import os 

85 start_tab = None 

86 html_file = kwargs.get('html_file', 

87 os.environ.get('FIREFLY_HTML', '')) 

88 if url is None: 

89 if (('fireflyLabExtension' in os.environ) and 

90 ('fireflyURLLab' in os.environ)): 

91 url = os.environ['fireflyURLLab'] 

92 start_tab = kwargs.get('start_tab', True) 

93 start_browser_tab = kwargs.get('start_browser_tab', False) 

94 if (name is None) and ('fireflyChannelLab' in os.environ): 

95 name = os.environ['fireflyChannelLab'] 

96 elif 'FIREFLY_URL' in os.environ: 

97 url = os.environ['FIREFLY_URL'] 

98 else: 

99 raise RuntimeError('Cannot determine url from environment; you must pass url') 

100 

101 token = kwargs.get('token', 

102 os.environ.get('ACCESS_TOKEN', None)) 

103 

104 try: 

105 if start_tab: 

106 if verbose: 

107 print('Starting Jupyterlab client') 

108 _fireflyClient = firefly_client.FireflyClient.make_lab_client( 

109 start_tab=True, start_browser_tab=start_browser_tab, 

110 html_file=html_file, verbose=verbose, 

111 token=token) 

112 

113 else: 

114 if verbose: 

115 print('Starting vanilla client') 

116 _fireflyClient = firefly_client.FireflyClient.make_client( 

117 url=url, html_file=html_file, launch_browser=True, 

118 channel_override=name, verbose=verbose, 

119 token=token) 

120 

121 except (HandshakeError, gaierror) as e: 

122 raise RuntimeError(f"Unable to connect to {url or ''}: {e}") 

123 

124 try: 

125 _fireflyClient.add_listener(self.__handleCallbacks) 

126 except Exception as e: 

127 raise RuntimeError("Cannot add listener. Browser must be connected" 

128 f"to {_fireflyClient.get_firefly_url()}: {e}") 

129 

130 self._isBuffered = False 

131 self._regions = [] 

132 self._regionLayerId = self._getRegionLayerId() 

133 self._fireflyFitsID = None 

134 self._fireflyMaskOnServer = None 

135 self._client = _fireflyClient 

136 self._channel = _fireflyClient.channel 

137 self._url = _fireflyClient.get_firefly_url() 

138 self._maskIds = [] 

139 self._maskDict = {} 

140 self._maskPlaneColors = {} 

141 self._maskTransparencies = {} 

142 self._lastZoom = None 

143 self._lastPan = None 

144 self._lastStretch = None 

145 

146 def _getRegionLayerId(self): 

147 return f"lsstRegions{self.display.frame}" if self.display else "None" 

148 

149 def _clearImage(self): 

150 """Delete the current image in the Firefly viewer 

151 """ 

152 self._client.dispatch(action_type='ImagePlotCntlr.deletePlotView', 

153 payload=dict(plotId=str(self.display.frame))) 

154 

155 def _mtv(self, image, mask=None, wcs=None, title="", metadata=None): 

156 """Display an Image and/or Mask on a Firefly display 

157 """ 

158 if title == "": 

159 title = str(self.display.frame) 

160 if image: 

161 if self.verbose: 

162 print('displaying image') 

163 self._erase() 

164 

165 with BytesIO() as fd: 

166 afwDisplay.writeFitsImage(fd, image, wcs, title, metadata=metadata) 

167 fd.seek(0, 0) 

168 self._fireflyFitsID = _fireflyClient.upload_fits_data(fd) 

169 

170 try: 

171 viewer_id = f'image-{_fireflyClient.render_tree_id}-{self.frame}' 

172 except AttributeError: 

173 viewer_id = f'image-{self.frame}' 

174 extraParams = dict(Title=title, 

175 MultiImageIdx=0, 

176 PredefinedOverlayIds=' ', 

177 viewer_id=viewer_id) 

178 # Firefly's Javascript API requires a space for parameters; 

179 # otherwise the parameter will be ignored 

180 

181 if self._lastZoom: 

182 extraParams['InitZoomLevel'] = self._lastZoom 

183 extraParams['ZoomType'] = 'LEVEL' 

184 if self._lastPan: 

185 extraParams['InitialCenterPosition'] = f'{self._lastPan[0]:.3f};{self._lastPan[1]:.3f};PIXEL' 

186 if self._lastStretch: 

187 extraParams['RangeValues'] = self._lastStretch 

188 

189 ret = _fireflyClient.show_fits_image(self._fireflyFitsID, plot_id=str(self.display.frame), 

190 **extraParams) 

191 

192 if not ret["success"]: 

193 raise RuntimeError("Display of image failed") 

194 

195 if mask: 

196 if self.verbose: 

197 print('displaying mask') 

198 with BytesIO() as fdm: 

199 afwDisplay.writeFitsImage(fdm, mask, wcs, title, metadata=metadata) 

200 fdm.seek(0, 0) 

201 self._fireflyMaskOnServer = _fireflyClient.upload_fits_data(fdm) 

202 

203 maskPlaneDict = mask.getMaskPlaneDict() 

204 for k, v in maskPlaneDict.items(): 

205 self._maskDict[k] = v 

206 self._maskPlaneColors[k] = self.display.getMaskPlaneColor(k) 

207 usedPlanes = int(afwMath.makeStatistics(mask, afwMath.SUM).getValue()) 

208 for k in self._maskDict: 

209 if (((1 << self._maskDict[k]) & usedPlanes) and 

210 (k in self._maskPlaneColors) and 

211 (self._maskPlaneColors[k] is not None) and 

212 (self._maskPlaneColors[k].lower() != 'ignore')): 

213 _fireflyClient.add_mask(bit_number=self._maskDict[k], 

214 image_number=0, 

215 plot_id=str(self.display.frame), 

216 mask_id=k, 

217 title=k + ' - bit %d'%self._maskDict[k], 

218 color=self._maskPlaneColors[k], 

219 file_on_server=self._fireflyMaskOnServer) 

220 if k in self._maskTransparencies: 

221 self._setMaskTransparency(self._maskTransparencies[k], k) 

222 self._maskIds.append(k) 

223 

224 def _remove_masks(self): 

225 """Remove mask layers""" 

226 for k in self._maskIds: 

227 _fireflyClient.remove_mask(plot_id=str(self.display.frame), mask_id=k) 

228 self._maskIds = [] 

229 

230 def _buffer(self, enable=True): 

231 """!Enable or disable buffering of writes to the display 

232 param enable True or False, as appropriate 

233 """ 

234 self._isBuffered = enable 

235 

236 def _flush(self): 

237 """!Flush any I/O buffers 

238 """ 

239 if not self._regions: 

240 return 

241 

242 if self.verbose: 

243 print("Flushing %d regions" % len(self._regions)) 

244 print(self._regions) 

245 

246 self._regionLayerId = self._getRegionLayerId() 

247 _fireflyClient.add_region_data(region_data=self._regions, plot_id=str(self.display.frame), 

248 region_layer_id=self._regionLayerId) 

249 self._regions = [] 

250 

251 def _uploadTextData(self, regions): 

252 self._regions += regions 

253 

254 if not self._isBuffered: 

255 self._flush() 

256 

257 def _close(self): 

258 """Called when the device is closed""" 

259 if self.verbose: 

260 print("Closing firefly device %s" % (self.display.frame if self.display else "[None]")) 

261 if _fireflyClient is not None: 

262 _fireflyClient.disconnect() 

263 _fireflyClient.session.close() 

264 

265 def _dot(self, symb, c, r, size, ctype, fontFamily="helvetica", textAngle=None): 

266 """Draw a symbol onto the specified DS9 frame at (col,row) = (c,r) [0-based coordinates] 

267 Possible values are: 

268 + Draw a + 

269 x Draw an x 

270 * Draw a * 

271 o Draw a circle 

272 @:Mxx,Mxy,Myy Draw an ellipse with moments (Mxx, Mxy, Myy) (argument size is ignored) 

273 An object derived from afwGeom.ellipses.BaseCore Draw the ellipse (argument size is ignored) 

274 Any other value is interpreted as a string to be drawn. Strings obey the fontFamily (which may be extended 

275 with other characteristics, e.g. "times bold italic". Text will be drawn rotated by textAngle (textAngle 

276 is ignored otherwise). 

277 

278 N.b. objects derived from BaseCore include Axes and Quadrupole. 

279 """ 

280 self._uploadTextData(ds9Regions.dot(symb, c, r, size, ctype, fontFamily, textAngle)) 

281 

282 def _drawLines(self, points, ctype): 

283 """Connect the points, a list of (col,row) 

284 Ctype is the name of a colour (e.g. 'red')""" 

285 

286 self._uploadTextData(ds9Regions.drawLines(points, ctype)) 

287 

288 def _erase(self): 

289 """Erase all overlays on the image""" 

290 if self.verbose: 

291 print(f'region layer id is {self._regionLayerId}') 

292 if self._regionLayerId: 

293 _fireflyClient.delete_region_layer(self._regionLayerId, plot_id=str(self.display.frame)) 

294 

295 def _setCallback(self, what, func): 

296 if func != interface.noop_callback: 

297 try: 

298 status = _fireflyClient.add_extension('POINT' if False else 'AREA_SELECT', title=what, 

299 plot_id=str(self.display.frame), 

300 extension_id=what) 

301 if not status['success']: 

302 pass 

303 except Exception as e: 

304 raise RuntimeError("Cannot set callback. Browser must be (re)opened " 

305 f"to {_fireflyClient.url_bw}{_fireflyClient.channel} : {e}") 

306 

307 def _getEvent(self): 

308 """Return an event generated by a keypress or mouse click 

309 """ 

310 ev = interface.Event("q") 

311 

312 if self.verbose: 

313 print(f"virtual[{self.display.frame}]._getEvent() -> {ev}") 

314 

315 return ev 

316 # 

317 # Set gray scale 

318 # 

319 

320 def _scale(self, algorithm, min, max, unit=None, *args, **kwargs): 

321 """Scale the image stretch and limits 

322 

323 Parameters: 

324 ----------- 

325 algorithm : `str` 

326 stretch algorithm, e.g. 'linear', 'log', 'loglog', 'equal', 'squared', 

327 'sqrt', 'asinh', powerlaw_gamma' 

328 min : `float` or `str` 

329 lower limit, or 'minmax' for full range, or 'zscale' 

330 max : `float` or `str` 

331 upper limit; overrriden if min is 'minmax' or 'zscale' 

332 unit : `str` 

333 unit for min and max. 'percent', 'absolute', 'sigma'. 

334 if not specified, min and max are presumed to be in 'absolute' units. 

335 

336 *args, **kwargs : additional position and keyword arguments. 

337 The options are shown below: 

338 

339 **Q** : `float`, optional 

340 The asinh softening parameter for asinh stretch. 

341 Use Q=0 for linear stretch, increase Q to make brighter features visible. 

342 When not specified or None, Q is calculated by Firefly to use full color range. 

343 **gamma** 

344 The gamma value for power law gamma stretch (default 2.0) 

345 **zscale_contrast** : `int`, optional 

346 Contrast parameter in percent for zscale algorithm (default 25) 

347 **zscale_samples** : `int`, optional 

348 Number of samples for zscale algorithm (default 600) 

349 **zscale_samples_perline** : `int`, optional 

350 Number of samples per line for zscale algorithm (default 120) 

351 """ 

352 stretch_algorithms = ('linear', 'log', 'loglog', 'equal', 'squared', 'sqrt', 

353 'asinh', 'powerlaw_gamma') 

354 interval_methods = ('percent', 'maxmin', 'absolute', 'zscale', 'sigma') 

355 # 

356 # 

357 # Normalise algorithm's case 

358 # 

359 if algorithm: 

360 algorithm = dict((a.lower(), a) for a in stretch_algorithms).get(algorithm.lower(), algorithm) 

361 

362 if algorithm not in stretch_algorithms: 

363 raise FireflyError( 

364 'Algorithm {} is invalid; please choose one of "{}"'.format( 

365 algorithm, '", "'.join(stretch_algorithms) 

366 ) 

367 ) 

368 self._stretchAlgorithm = algorithm 

369 else: 

370 algorithm = 'linear' 

371 

372 # Translate parameters for asinh and powerlaw_gamma stretches 

373 if 'Q' in kwargs: 

374 kwargs['asinh_q_value'] = kwargs['Q'] 

375 del kwargs['Q'] 

376 

377 if 'gamma' in kwargs: 

378 kwargs['gamma_value'] = kwargs['gamma'] 

379 del kwargs['gamma'] 

380 

381 if min == 'minmax': 

382 interval_type = 'percent' 

383 unit = 'percent' 

384 min, max = 0, 100 

385 elif min == 'zscale': 

386 interval_type = 'zscale' 

387 else: 

388 interval_type = None 

389 

390 if not unit: 

391 unit = 'absolute' 

392 

393 units = ('percent', 'absolute', 'sigma') 

394 if unit not in units: 

395 raise FireflyError( 

396 'Unit {} is invalid; please choose one of "{}"'.format(unit, '", "'.join(units)) 

397 ) 

398 

399 if unit == 'sigma': 

400 interval_type = 'sigma' 

401 elif unit == 'absolute' and interval_type is None: 

402 interval_type = 'absolute' 

403 elif unit == 'percent': 

404 interval_type = 'percent' 

405 

406 self._stretchMin = min 

407 self._stretchMax = max 

408 self._stretchUnit = unit 

409 

410 if interval_type not in interval_methods: 

411 raise FireflyError(f'Interval method {interval_type} is invalid') 

412 

413 rval = {} 

414 if interval_type != 'zscale': 

415 rval = _fireflyClient.set_stretch(str(self.display.frame), stype=interval_type, 

416 algorithm=algorithm, lower_value=min, 

417 upper_value=max, **kwargs) 

418 else: 

419 if 'zscale_contrast' not in kwargs: 

420 kwargs['zscale_contrast'] = 25 

421 if 'zscale_samples' not in kwargs: 

422 kwargs['zscale_samples'] = 600 

423 if 'zscale_samples_perline' not in kwargs: 

424 kwargs['zscale_samples_perline'] = 120 

425 rval = _fireflyClient.set_stretch(str(self.display.frame), stype='zscale', 

426 algorithm=algorithm, **kwargs) 

427 

428 if 'rv_string' in rval: 

429 self._lastStretch = rval['rv_string'] 

430 

431 def _setMaskTransparency(self, transparency, maskName): 

432 """Specify mask transparency (percent); or None to not set it when loading masks""" 

433 if maskName is not None: 

434 masklist = [maskName] 

435 else: 

436 masklist = set(self._maskIds + list(self.display._defaultMaskPlaneColor.keys())) 

437 for k in masklist: 

438 self._maskTransparencies[k] = transparency 

439 _fireflyClient.dispatch(action_type='ImagePlotCntlr.overlayPlotChangeAttributes', 

440 payload={'plotId': str(self.display.frame), 

441 'imageOverlayId': k, 

442 'attributes': {'opacity': 1.0 - transparency/100.}, 

443 'doReplot': False}) 

444 

445 def _getMaskTransparency(self, maskName): 

446 """Return the current mask's transparency""" 

447 transparency = None 

448 if maskName in self._maskTransparencies: 

449 transparency = self._maskTransparencies[maskName] 

450 return transparency 

451 

452 def _setMaskPlaneColor(self, maskName, color): 

453 """Specify mask color """ 

454 _fireflyClient.remove_mask(plot_id=str(self.display.frame), 

455 mask_id=maskName) 

456 self._maskPlaneColors[maskName] = color 

457 if (color.lower() != 'ignore'): 

458 _fireflyClient.add_mask(bit_number=self._maskDict[maskName], 

459 image_number=1, 

460 plot_id=str(self.display.frame), 

461 mask_id=maskName, 

462 color=self.display.getMaskPlaneColor(maskName), 

463 file_on_server=self._fireflyFitsID) 

464 

465 def _show(self): 

466 """Show the requested window""" 

467 if self._client.render_tree_id is not None: 

468 # we are using Jupyterlab 

469 self._client.dispatch(self._client.ACTION_DICT['StartLabWindow'], 

470 {}) 

471 else: 

472 localbrowser, url = _fireflyClient.launch_browser(verbose=self.verbose) 

473 if not localbrowser and not self.verbose: 

474 _fireflyClient.display_url() 

475 

476 # 

477 # Zoom and Pan 

478 # 

479 

480 def _zoom(self, zoomfac): 

481 """Zoom display by specified amount 

482 

483 Parameters: 

484 ----------- 

485 zoomfac: `float` 

486 zoom level in screen pixels per image pixel 

487 """ 

488 self._lastZoom = zoomfac 

489 _fireflyClient.set_zoom(plot_id=str(self.display.frame), factor=zoomfac) 

490 

491 def _pan(self, colc, rowc): 

492 """Pan to specified pixel coordinates 

493 

494 Parameters: 

495 ----------- 

496 colc, rowc : `float` 

497 column and row in units of pixels (zero-based convention, 

498 with the xy0 already subtracted off) 

499 """ 

500 self._lastPan = [colc+0.5, rowc+0.5] # saved for future use in _mtv 

501 # Firefly's internal convention is first pixel is (0.5, 0.5) 

502 _fireflyClient.set_pan(plot_id=str(self.display.frame), x=colc, y=rowc) 

503 

504 # Extensions to the API that are specific to using the Firefly backend 

505 

506 def getClient(self): 

507 """Get the instance of FireflyClient for this display 

508 

509 Returns: 

510 -------- 

511 `firefly_client.FireflyClient` 

512 Instance of FireflyClient used by this display 

513 """ 

514 return self._client 

515 

516 def clearViewer(self): 

517 """Reinitialize the viewer 

518 """ 

519 self._client.reinit_viewer() 

520 

521 def resetLayout(self): 

522 """Reset the layout of the Firefly Slate browser 

523 

524 Clears the display and adds Slate cells to display image in upper left, 

525 plot area in upper right, and plots stretch across the bottom 

526 """ 

527 self.clearViewer() 

528 try: 

529 tables_cell_id = 'tables-' + str(_fireflyClient.render_tree_id) 

530 except AttributeError: 

531 tables_cell_id = 'tables' 

532 self._client.add_cell(row=2, col=0, width=4, height=2, element_type='tables', 

533 cell_id=tables_cell_id) 

534 try: 

535 image_cell_id = ('image-' + str(_fireflyClient.render_tree_id) + '-' + 

536 str(self.frame)) 

537 except AttributeError: 

538 image_cell_id = 'image-' + str(self.frame) 

539 self._client.add_cell(row=0, col=0, width=2, height=3, element_type='images', 

540 cell_id=image_cell_id) 

541 try: 

542 plots_cell_id = 'plots-' + str(_fireflyClient.render_tree_id) 

543 except AttributeError: 

544 plots_cell_id = 'plots' 

545 self._client.add_cell(row=0, col=2, width=2, height=3, element_type='xyPlots', 

546 cell_id=plots_cell_id) 

547 

548 def overlayFootprints(self, catalog, color='rgba(74,144,226,0.60)', 

549 highlightColor='cyan', selectColor='orange', 

550 style='fill', layerString='detection footprints ', 

551 titleString='catalog footprints '): 

552 """Overlay outlines of footprints from a catalog 

553 

554 Overlay outlines of LSST footprints from the input catalog. The colors 

555 and style can be specified as parameters, and the base color and style 

556 can be changed in the Firefly browser user interface. 

557 

558 Parameters: 

559 ----------- 

560 catalog : `lsst.afw.table.SourceCatalog` 

561 Source catalog from which to display footprints. 

562 color : `str` 

563 Color for footprints overlay. Colors can be specified as a name 

564 like 'cyan' or afwDisplay.RED; as an rgb value such as 

565 'rgb(80,100,220)'; or as rgb plus alpha (transparency) such 

566 as 'rgba('74,144,226,0.60)'. 

567 highlightColor : `str` 

568 Color for highlighted footprints 

569 selectColor : `str` 

570 Color for selected footprints 

571 style : {'fill', 'outline'} 

572 Style of footprints display, filled or outline 

573 insertColumn : `int` 

574 Column at which to insert the "family_id" and "category" columns 

575 layerString: `str` 

576 Name of footprints layer string, to concatenate with the frame 

577 Re-using the layer_string will overwrite the previous table and 

578 footprints 

579 titleString: `str` 

580 Title of catalog, to concatenate with the frame 

581 """ 

582 footprintTable = createFootprintsTable(catalog) 

583 with BytesIO() as fd: 

584 footprintTable.to_xml(fd) 

585 tableval = self._client.upload_data(fd, 'UNKNOWN') 

586 self._client.overlay_footprints(footprint_file=tableval, 

587 title=titleString + str(self.display.frame), 

588 footprint_layer_id=layerString + str(self.display.frame), 

589 plot_id=str(self.display.frame), 

590 color=color, 

591 highlightColor=highlightColor, 

592 selectColor=selectColor, 

593 style=style) 

594 

595 def alignImages(self, match_type="Standard", lock_match=True): 

596 """Align and optionally lock the orientation of the images being 

597 displayed. 

598 

599 See the Firefly native docs for additional kwargs reference: 

600 https://caltech-ipac.github.io/firefly_client/api/firefly_client.FireflyClient.html#firefly_client.FireflyClient.align_images 

601 

602 Parameters 

603 ---------- 

604 match_type : `str`, optional 

605 Match type to use to align the images: align by WCS (‘Standard’), 

606 by target (‘Target’), by pixel prigins (‘Pixel’), and by pixel at 

607 image centers (‘PixelCenter’). 

608 lock_match : `bool`, optional 

609 Whether to lock the alignment. Panning/zooming in one image will 

610 preserve the alignment in other images. 

611 

612 Returns 

613 ------- 

614 out : `dict` 

615 Status of the request. 

616 

617 Raises 

618 ------ 

619 ValueError 

620 Raised if match_type is not one of the allowed values. 

621 """ 

622 types = {"Standard", "Target", "Pixel", "PixelCenter"} 

623 if match_type not in types: 

624 raise ValueError(f"match_type={match_type} not allowed from expected types: {types}.") 

625 

626 return self._client.align_images(match_type=match_type, lock_match=lock_match)