plyfile.py 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. # Copyright 2014-2020 Darsh Ranjan
  2. #
  3. # This file is part of python-plyfile.
  4. #
  5. # python-plyfile is free software: you can redistribute it and/or
  6. # modify it under the terms of the GNU 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. # python-plyfile 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 GNU
  13. # General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with python-plyfile. If not, see
  17. # <http://www.gnu.org/licenses/>.
  18. from itertools import islice as _islice
  19. import numpy as _np
  20. from sys import byteorder as _byteorder
  21. try:
  22. _range = xrange
  23. except NameError:
  24. _range = range
  25. # Many-many relation
  26. _data_type_relation = [
  27. ('int8', 'i1'),
  28. ('char', 'i1'),
  29. ('uint8', 'u1'),
  30. ('uchar', 'b1'),
  31. ('uchar', 'u1'),
  32. ('int16', 'i2'),
  33. ('short', 'i2'),
  34. ('uint16', 'u2'),
  35. ('ushort', 'u2'),
  36. ('int32', 'i4'),
  37. ('int', 'i4'),
  38. ('uint32', 'u4'),
  39. ('uint', 'u4'),
  40. ('float32', 'f4'),
  41. ('float', 'f4'),
  42. ('float64', 'f8'),
  43. ('double', 'f8')
  44. ]
  45. _data_types = dict(_data_type_relation)
  46. _data_type_reverse = dict((b, a) for (a, b) in _data_type_relation)
  47. _types_list = []
  48. _types_set = set()
  49. for (_a, _b) in _data_type_relation:
  50. if _a not in _types_set:
  51. _types_list.append(_a)
  52. _types_set.add(_a)
  53. if _b not in _types_set:
  54. _types_list.append(_b)
  55. _types_set.add(_b)
  56. _byte_order_map = {
  57. 'ascii': '=',
  58. 'binary_little_endian': '<',
  59. 'binary_big_endian': '>'
  60. }
  61. _byte_order_reverse = {
  62. '<': 'binary_little_endian',
  63. '>': 'binary_big_endian'
  64. }
  65. _native_byte_order = {'little': '<', 'big': '>'}[_byteorder]
  66. def _lookup_type(type_str):
  67. if type_str not in _data_type_reverse:
  68. try:
  69. type_str = _data_types[type_str]
  70. except KeyError:
  71. raise ValueError("field type %r not in %r" %
  72. (type_str, _types_list))
  73. return _data_type_reverse[type_str]
  74. def make2d(array, cols=None, dtype=None):
  75. '''
  76. Make a 2D array from an array of arrays. The `cols' and `dtype'
  77. arguments can be omitted if the array is not empty.
  78. '''
  79. if not len(array):
  80. if cols is None or dtype is None:
  81. raise RuntimeError(
  82. "cols and dtype must be specified for empty array"
  83. )
  84. return _np.empty((0, cols), dtype=dtype)
  85. return _np.vstack(array)
  86. class _PlyHeaderParser(object):
  87. def __init__(self):
  88. self.format = None
  89. self.elements = []
  90. self.comments = []
  91. self.obj_info = []
  92. self.lines = 0
  93. self._allowed = ['ply']
  94. def consume(self, raw_line):
  95. self.lines += 1
  96. if not raw_line:
  97. self._error("early end-of-file")
  98. line = raw_line.decode('ascii').strip()
  99. try:
  100. keyword = line.split(None, 1)[0]
  101. except IndexError:
  102. self._error()
  103. if keyword not in self._allowed:
  104. self._error("expected one of {%s}" %
  105. ", ".join(self._allowed))
  106. getattr(self, 'parse_' + keyword)(line[len(keyword)+1:])
  107. return self._allowed
  108. def _error(self, message="parse error"):
  109. raise PlyHeaderParseError(message, self.lines)
  110. def parse_ply(self, data):
  111. if data:
  112. self._error("unexpected characters after 'ply'")
  113. self._allowed = ['format', 'comment', 'obj_info']
  114. def parse_format(self, data):
  115. fields = data.strip().split()
  116. if len(fields) != 2:
  117. self._error("expected \"format {format} 1.0\"")
  118. self.format = fields[0]
  119. if self.format not in _byte_order_map:
  120. self._error("don't understand format %r" % format)
  121. if fields[1] != '1.0':
  122. self._error("expected version '1.0'")
  123. self._allowed = ['element', 'comment', 'obj_info', 'end_header']
  124. def parse_comment(self, data):
  125. if not self.elements:
  126. self.comments.append(data)
  127. else:
  128. self.elements[-1][3].append(data)
  129. def parse_obj_info(self, data):
  130. self.obj_info.append(data)
  131. def parse_element(self, data):
  132. fields = data.strip().split()
  133. if len(fields) != 2:
  134. self._error("expected \"element {name} {count}\"")
  135. name = fields[0]
  136. try:
  137. count = int(fields[1])
  138. except ValueError:
  139. self._error("expected integer count")
  140. self.elements.append((name, [], count, []))
  141. self._allowed = ['element', 'comment', 'property', 'end_header']
  142. def parse_property(self, data):
  143. properties = self.elements[-1][1]
  144. fields = data.strip().split()
  145. if len(fields) < 2:
  146. self._error("bad 'property' line")
  147. if fields[0] == 'list':
  148. if len(fields) != 4:
  149. self._error("expected \"property list "
  150. "{len_type} {val_type} {name}\"")
  151. try:
  152. properties.append(
  153. PlyListProperty(fields[3], fields[1], fields[2])
  154. )
  155. except ValueError as e:
  156. self._error(str(e))
  157. else:
  158. if len(fields) != 2:
  159. self._error("expected \"property {type} {name}\"")
  160. try:
  161. properties.append(
  162. PlyProperty(fields[1], fields[0])
  163. )
  164. except ValueError as e:
  165. self._error(str(e))
  166. def parse_end_header(self, data):
  167. if data:
  168. self._error("unexpected data after 'end_header'")
  169. self._allowed = []
  170. class PlyParseError(Exception):
  171. '''
  172. Base class for PLY parsing errors.
  173. '''
  174. pass
  175. class PlyElementParseError(PlyParseError):
  176. '''
  177. Raised when a PLY element cannot be parsed.
  178. The attributes `element', `row', `property', and `message' give
  179. additional information.
  180. '''
  181. def __init__(self, message, element=None, row=None, prop=None):
  182. self.message = message
  183. self.element = element
  184. self.row = row
  185. self.prop = prop
  186. s = ''
  187. if self.element:
  188. s += 'element %r: ' % self.element.name
  189. if self.row is not None:
  190. s += 'row %d: ' % self.row
  191. if self.prop:
  192. s += 'property %r: ' % self.prop.name
  193. s += self.message
  194. Exception.__init__(self, s)
  195. def __repr__(self):
  196. return ('%s(%r, element=%r, row=%r, prop=%r)' %
  197. (self.__class__.__name__,
  198. self.message, self.element, self.row, self.prop))
  199. class PlyHeaderParseError(PlyParseError):
  200. '''
  201. Raised when a PLY header cannot be parsed.
  202. The attribute `line' provides additional information.
  203. '''
  204. def __init__(self, message, line=None):
  205. self.message = message
  206. self.line = line
  207. s = ''
  208. if self.line:
  209. s += 'line %r: ' % self.line
  210. s += self.message
  211. Exception.__init__(self, s)
  212. def __repr__(self):
  213. return ('%s(%r, line=%r)' %
  214. (self.__class__.__name__,
  215. self.message, self.line))
  216. class PlyData(object):
  217. '''
  218. PLY file header and data.
  219. A PlyData instance is created in one of two ways: by the static
  220. method PlyData.read (to read a PLY file), or directly from __init__
  221. given a sequence of elements (which can then be written to a PLY
  222. file).
  223. '''
  224. def __init__(self, elements=[], text=False, byte_order='=',
  225. comments=[], obj_info=[]):
  226. '''
  227. elements: sequence of PlyElement instances.
  228. text: whether the resulting PLY file will be text (True) or
  229. binary (False).
  230. byte_order: '<' for little-endian, '>' for big-endian, or '='
  231. for native. This is only relevant if `text' is False.
  232. comments: sequence of strings that will be placed in the header
  233. between the 'ply' and 'format ...' lines.
  234. obj_info: like comments, but will be placed in the header with
  235. "obj_info ..." instead of "comment ...".
  236. '''
  237. if byte_order == '=' and not text:
  238. byte_order = _native_byte_order
  239. self.byte_order = byte_order
  240. self.text = text
  241. self.comments = comments
  242. self.obj_info = obj_info
  243. self.elements = elements
  244. def _get_elements(self):
  245. return self._elements
  246. def _set_elements(self, elements):
  247. self._elements = tuple(elements)
  248. self._index()
  249. elements = property(_get_elements, _set_elements)
  250. def _get_byte_order(self):
  251. return self._byte_order
  252. def _set_byte_order(self, byte_order):
  253. if byte_order not in ['<', '>', '=']:
  254. raise ValueError("byte order must be '<', '>', or '='")
  255. self._byte_order = byte_order
  256. byte_order = property(_get_byte_order, _set_byte_order)
  257. def _index(self):
  258. self._element_lookup = dict((elt.name, elt) for elt in
  259. self._elements)
  260. if len(self._element_lookup) != len(self._elements):
  261. raise ValueError("two elements with same name")
  262. def _get_comments(self):
  263. return list(self._comments)
  264. def _set_comments(self, comments):
  265. _check_comments(comments)
  266. self._comments = list(comments)
  267. comments = property(_get_comments, _set_comments)
  268. def _get_obj_info(self):
  269. return list(self._obj_info)
  270. def _set_obj_info(self, obj_info):
  271. _check_comments(obj_info)
  272. self._obj_info = list(obj_info)
  273. obj_info = property(_get_obj_info, _set_obj_info)
  274. @staticmethod
  275. def _parse_header(stream):
  276. '''
  277. Parse a PLY header from a readable file-like stream.
  278. '''
  279. parser = _PlyHeaderParser()
  280. while parser.consume(stream.readline()):
  281. pass
  282. return PlyData(
  283. [PlyElement(*e) for e in parser.elements],
  284. parser.format == 'ascii',
  285. _byte_order_map[parser.format],
  286. parser.comments,
  287. parser.obj_info
  288. )
  289. @staticmethod
  290. def read(stream, mmap=True, known_list_len={}):
  291. '''
  292. Read PLY data from a readable file-like object or filename.
  293. mmap (optional): whether to allow element data to be
  294. memory-mapped when possible. The default is True, which
  295. allows memory mapping. Using False will prevent memory
  296. mapping.
  297. known_list_len (optional): mapping from element names to
  298. mappings from list property names to their fixed lengths.
  299. This optional argument is necessary to enable memory mapping
  300. of elements that contain list properties. (Note that
  301. elements with variable-length list properties cannot be
  302. memory-mapped.)
  303. '''
  304. (must_close, stream) = _open_stream(stream, 'read')
  305. try:
  306. data = PlyData._parse_header(stream)
  307. for elt in data:
  308. elt._read(stream, data.text, data.byte_order, mmap,
  309. known_list_len=known_list_len.get(elt.name, {}))
  310. finally:
  311. if must_close:
  312. stream.close()
  313. return data
  314. def write(self, stream):
  315. '''
  316. Write PLY data to a writeable file-like object or filename.
  317. '''
  318. (must_close, stream) = _open_stream(stream, 'write')
  319. try:
  320. stream.write(self.header.encode('ascii'))
  321. stream.write(b'\n')
  322. for elt in self:
  323. elt._write(stream, self.text, self.byte_order)
  324. finally:
  325. if must_close:
  326. stream.close()
  327. @property
  328. def header(self):
  329. '''
  330. Provide PLY-formatted metadata for the instance.
  331. '''
  332. lines = ['ply']
  333. if self.text:
  334. lines.append('format ascii 1.0')
  335. else:
  336. lines.append('format ' +
  337. _byte_order_reverse[self.byte_order] +
  338. ' 1.0')
  339. # Some information is lost here, since all comments are placed
  340. # between the 'format' line and the first element.
  341. for c in self.comments:
  342. lines.append('comment ' + c)
  343. for c in self.obj_info:
  344. lines.append('obj_info ' + c)
  345. lines.extend(elt.header for elt in self.elements)
  346. lines.append('end_header')
  347. return '\n'.join(lines)
  348. def __iter__(self):
  349. return iter(self.elements)
  350. def __len__(self):
  351. return len(self.elements)
  352. def __contains__(self, name):
  353. return name in self._element_lookup
  354. def __getitem__(self, name):
  355. return self._element_lookup[name]
  356. def __str__(self):
  357. return self.header
  358. def __repr__(self):
  359. return ('PlyData(%r, text=%r, byte_order=%r, '
  360. 'comments=%r, obj_info=%r)' %
  361. (self.elements, self.text, self.byte_order,
  362. self.comments, self.obj_info))
  363. def _open_stream(stream, read_or_write):
  364. if hasattr(stream, read_or_write):
  365. return (False, stream)
  366. try:
  367. return (True, open(stream, read_or_write[0] + 'b'))
  368. except TypeError:
  369. raise RuntimeError("expected open file or filename")
  370. class PlyElement(object):
  371. '''
  372. PLY file element.
  373. A client of this library doesn't normally need to instantiate this
  374. directly, so the following is only for the sake of documenting the
  375. internals.
  376. Creating a PlyElement instance is generally done in one of two ways:
  377. as a byproduct of PlyData.read (when reading a PLY file) and by
  378. PlyElement.describe (before writing a PLY file).
  379. '''
  380. def __init__(self, name, properties, count, comments=[]):
  381. '''
  382. This is not part of the public interface. The preferred methods
  383. of obtaining PlyElement instances are PlyData.read (to read from
  384. a file) and PlyElement.describe (to construct from a numpy
  385. array).
  386. '''
  387. _check_name(name)
  388. self._name = str(name)
  389. self._count = count
  390. self._properties = tuple(properties)
  391. self._index()
  392. self.comments = comments
  393. self._have_list = any(isinstance(p, PlyListProperty)
  394. for p in self.properties)
  395. @property
  396. def count(self):
  397. return self._count
  398. def _get_data(self):
  399. return self._data
  400. def _set_data(self, data):
  401. self._data = data
  402. self._count = len(data)
  403. self._check_sanity()
  404. data = property(_get_data, _set_data)
  405. def _check_sanity(self):
  406. for prop in self.properties:
  407. if prop.name not in self._data.dtype.fields:
  408. raise ValueError("dangling property %r" % prop.name)
  409. def _get_properties(self):
  410. return self._properties
  411. def _set_properties(self, properties):
  412. self._properties = tuple(properties)
  413. self._check_sanity()
  414. self._index()
  415. properties = property(_get_properties, _set_properties)
  416. def _get_comments(self):
  417. return list(self._comments)
  418. def _set_comments(self, comments):
  419. _check_comments(comments)
  420. self._comments = list(comments)
  421. comments = property(_get_comments, _set_comments)
  422. def _index(self):
  423. self._property_lookup = dict((prop.name, prop)
  424. for prop in self._properties)
  425. if len(self._property_lookup) != len(self._properties):
  426. raise ValueError("two properties with same name")
  427. def ply_property(self, name):
  428. return self._property_lookup[name]
  429. @property
  430. def name(self):
  431. return self._name
  432. def dtype(self, byte_order='='):
  433. '''
  434. Return the numpy dtype of the in-memory representation of the
  435. data. (If there are no list properties, and the PLY format is
  436. binary, then this also accurately describes the on-disk
  437. representation of the element.)
  438. '''
  439. return _np.dtype([(prop.name, prop.dtype(byte_order))
  440. for prop in self.properties])
  441. @staticmethod
  442. def describe(data, name, len_types={}, val_types={},
  443. comments=[]):
  444. '''
  445. Construct a PlyElement from an array's metadata.
  446. len_types and val_types can be given as mappings from list
  447. property names to type strings (like 'u1', 'f4', etc., or
  448. 'int8', 'float32', etc.). These can be used to define the length
  449. and value types of list properties. List property lengths
  450. always default to type 'u1' (8-bit unsigned integer), and value
  451. types default to 'i4' (32-bit integer).
  452. '''
  453. if not isinstance(data, _np.ndarray):
  454. raise TypeError("only numpy arrays are supported")
  455. if len(data.shape) != 1:
  456. raise ValueError("only one-dimensional arrays are "
  457. "supported")
  458. count = len(data)
  459. properties = []
  460. descr = data.dtype.descr
  461. for t in descr:
  462. if not isinstance(t[1], str):
  463. raise ValueError("nested records not supported")
  464. if not t[0]:
  465. raise ValueError("field with empty name")
  466. if len(t) != 2 or t[1][1] == 'O':
  467. # non-scalar field, which corresponds to a list
  468. # property in PLY.
  469. if t[1][1] == 'O':
  470. if len(t) != 2:
  471. raise ValueError("non-scalar object fields not "
  472. "supported")
  473. len_str = _data_type_reverse[len_types.get(t[0], 'u1')]
  474. if t[1][1] == 'O':
  475. val_type = val_types.get(t[0], 'i4')
  476. val_str = _lookup_type(val_type)
  477. else:
  478. val_str = _lookup_type(t[1][1:])
  479. prop = PlyListProperty(t[0], len_str, val_str)
  480. else:
  481. val_str = _lookup_type(t[1][1:])
  482. prop = PlyProperty(t[0], val_str)
  483. properties.append(prop)
  484. elt = PlyElement(name, properties, count, comments)
  485. elt.data = data
  486. return elt
  487. def _read(self, stream, text, byte_order, mmap,
  488. known_list_len={}):
  489. '''
  490. Read the actual data from a PLY file.
  491. '''
  492. if text:
  493. self._read_txt(stream)
  494. else:
  495. if known_list_len is None:
  496. known_list_len = {}
  497. list_prop_names = set(p.name for p in self.properties
  498. if isinstance(p, PlyListProperty))
  499. can_mmap_lists = list_prop_names <= set(known_list_len)
  500. if mmap and _can_mmap(stream) and can_mmap_lists:
  501. # Loading the data is straightforward. We will memory
  502. # map the file in copy-on-write mode.
  503. self._read_mmap(stream, byte_order, known_list_len)
  504. else:
  505. # A simple load is impossible.
  506. self._read_bin(stream, byte_order)
  507. self._check_sanity()
  508. def _write(self, stream, text, byte_order):
  509. '''
  510. Write the data to a PLY file.
  511. '''
  512. if text:
  513. self._write_txt(stream)
  514. else:
  515. if self._have_list:
  516. # There are list properties, so serialization is
  517. # slightly complicated.
  518. self._write_bin(stream, byte_order)
  519. else:
  520. # no list properties, so serialization is
  521. # straightforward.
  522. stream.write(self.data.astype(self.dtype(byte_order),
  523. copy=False).data)
  524. def _read_mmap(self, stream, byte_order, known_list_len):
  525. list_len_props = {}
  526. # update the dtype to include the list length and list dtype
  527. new_dtype = []
  528. for p in self.properties:
  529. if isinstance(p, PlyListProperty):
  530. # create new dtype for the list length
  531. new_dtype.append(
  532. (p.name + "\nlen", byte_order + p.len_dtype)
  533. )
  534. # a new dtype with size for the list values themselves
  535. new_dtype.append(
  536. (p.name, byte_order + p.val_dtype,
  537. (known_list_len[p.name],))
  538. )
  539. list_len_props[p.name] = p.name + "\nlen"
  540. else:
  541. new_dtype.append(
  542. (p.name, p.dtype(byte_order))
  543. )
  544. dtype = _np.dtype(new_dtype)
  545. num_bytes = self.count * dtype.itemsize
  546. offset = stream.tell()
  547. stream.seek(0, 2)
  548. max_bytes = stream.tell() - offset
  549. if max_bytes < num_bytes:
  550. raise PlyElementParseError("early end-of-file", self,
  551. max_bytes // dtype.itemsize)
  552. self._data = _np.memmap(stream, dtype, 'c', offset, self.count)
  553. # Fix stream position
  554. stream.seek(offset + self.count * dtype.itemsize)
  555. # remove any extra properties added
  556. for prop in list_len_props:
  557. field = list_len_props[prop]
  558. len_check = self._data[field] == known_list_len[prop]
  559. if not len_check.all():
  560. row = _np.flatnonzero(len_check ^ True)[0]
  561. raise PlyElementParseError(
  562. "unexpected list length",
  563. self, row, self.ply_property(prop))
  564. props = [p.name for p in self.properties]
  565. self._data = self._data[props]
  566. def _read_txt(self, stream):
  567. '''
  568. Load a PLY element from an ASCII-format PLY file. The element
  569. may contain list properties.
  570. '''
  571. self._data = _np.empty(self.count, dtype=self.dtype())
  572. k = 0
  573. for line in _islice(iter(stream.readline, b''), self.count):
  574. fields = iter(line.strip().split())
  575. for prop in self.properties:
  576. try:
  577. self._data[prop.name][k] = prop._from_fields(fields)
  578. except StopIteration:
  579. raise PlyElementParseError("early end-of-line",
  580. self, k, prop)
  581. except ValueError:
  582. raise PlyElementParseError("malformed input",
  583. self, k, prop)
  584. try:
  585. next(fields)
  586. except StopIteration:
  587. pass
  588. else:
  589. raise PlyElementParseError("expected end-of-line",
  590. self, k)
  591. k += 1
  592. if k < self.count:
  593. del self._data
  594. raise PlyElementParseError("early end-of-file", self, k)
  595. def _write_txt(self, stream):
  596. '''
  597. Save a PLY element to an ASCII-format PLY file. The element may
  598. contain list properties.
  599. '''
  600. for rec in self.data:
  601. fields = []
  602. for prop in self.properties:
  603. fields.extend(prop._to_fields(rec[prop.name]))
  604. _np.savetxt(stream, [fields], '%.18g', newline='\n')
  605. def _read_bin(self, stream, byte_order):
  606. '''
  607. Load a PLY element from a binary PLY file. The element may
  608. contain list properties.
  609. '''
  610. self._data = _np.empty(self.count, dtype=self.dtype(byte_order))
  611. for k in _range(self.count):
  612. for prop in self.properties:
  613. try:
  614. self._data[prop.name][k] = \
  615. prop._read_bin(stream, byte_order)
  616. except StopIteration:
  617. raise PlyElementParseError("early end-of-file",
  618. self, k, prop)
  619. def _write_bin(self, stream, byte_order):
  620. '''
  621. Save a PLY element to a binary PLY file. The element may
  622. contain list properties.
  623. '''
  624. for rec in self.data:
  625. for prop in self.properties:
  626. prop._write_bin(rec[prop.name], stream, byte_order)
  627. @property
  628. def header(self):
  629. '''
  630. Format this element's metadata as it would appear in a PLY
  631. header.
  632. '''
  633. lines = ['element %s %d' % (self.name, self.count)]
  634. # Some information is lost here, since all comments are placed
  635. # between the 'element' line and the first property definition.
  636. for c in self.comments:
  637. lines.append('comment ' + c)
  638. lines.extend(list(map(str, self.properties)))
  639. return '\n'.join(lines)
  640. def __len__(self):
  641. return self.count
  642. def __contains__(self, name):
  643. return name in self._property_lookup
  644. def __getitem__(self, key):
  645. return self.data[key]
  646. def __setitem__(self, key, value):
  647. self.data[key] = value
  648. def __str__(self):
  649. return self.header
  650. def __repr__(self):
  651. return ('PlyElement(%r, %r, count=%d, comments=%r)' %
  652. (self.name, self.properties, self.count,
  653. self.comments))
  654. def _check_comments(comments):
  655. for comment in comments:
  656. for char in comment:
  657. if not 0 <= ord(char) < 128:
  658. raise ValueError("non-ASCII character in comment")
  659. if char == '\n':
  660. raise ValueError("embedded newline in comment")
  661. class PlyProperty(object):
  662. '''
  663. PLY property description. This class is pure metadata; the data
  664. itself is contained in PlyElement instances.
  665. '''
  666. def __init__(self, name, val_dtype):
  667. _check_name(name)
  668. self._name = str(name)
  669. self.val_dtype = val_dtype
  670. def _get_val_dtype(self):
  671. return self._val_dtype
  672. def _set_val_dtype(self, val_dtype):
  673. self._val_dtype = _data_types[_lookup_type(val_dtype)]
  674. val_dtype = property(_get_val_dtype, _set_val_dtype)
  675. @property
  676. def name(self):
  677. return self._name
  678. def dtype(self, byte_order='='):
  679. '''
  680. Return the numpy dtype description for this property (as a tuple
  681. of strings).
  682. '''
  683. return byte_order + self.val_dtype
  684. def _from_fields(self, fields):
  685. '''
  686. Parse from generator. Raise StopIteration if the property could
  687. not be read.
  688. '''
  689. return _np.dtype(self.dtype()).type(next(fields))
  690. def _to_fields(self, data):
  691. '''
  692. Return generator over one item.
  693. '''
  694. yield _np.dtype(self.dtype()).type(data)
  695. def _read_bin(self, stream, byte_order):
  696. '''
  697. Read data from a binary stream. Raise StopIteration if the
  698. property could not be read.
  699. '''
  700. try:
  701. return _read_array(stream, self.dtype(byte_order), 1)[0]
  702. except IndexError:
  703. raise StopIteration
  704. def _write_bin(self, data, stream, byte_order):
  705. '''
  706. Write data to a binary stream.
  707. '''
  708. _write_array(stream, _np.dtype(self.dtype(byte_order)).type(data))
  709. def __str__(self):
  710. val_str = _data_type_reverse[self.val_dtype]
  711. return 'property %s %s' % (val_str, self.name)
  712. def __repr__(self):
  713. return 'PlyProperty(%r, %r)' % (self.name,
  714. _lookup_type(self.val_dtype))
  715. class PlyListProperty(PlyProperty):
  716. '''
  717. PLY list property description.
  718. '''
  719. def __init__(self, name, len_dtype, val_dtype):
  720. PlyProperty.__init__(self, name, val_dtype)
  721. self.len_dtype = len_dtype
  722. def _get_len_dtype(self):
  723. return self._len_dtype
  724. def _set_len_dtype(self, len_dtype):
  725. self._len_dtype = _data_types[_lookup_type(len_dtype)]
  726. len_dtype = property(_get_len_dtype, _set_len_dtype)
  727. def dtype(self, byte_order='='):
  728. '''
  729. List properties always have a numpy dtype of "object".
  730. '''
  731. return '|O'
  732. def list_dtype(self, byte_order='='):
  733. '''
  734. Return the pair (len_dtype, val_dtype) (both numpy-friendly
  735. strings).
  736. '''
  737. return (byte_order + self.len_dtype,
  738. byte_order + self.val_dtype)
  739. def _from_fields(self, fields):
  740. (len_t, val_t) = self.list_dtype()
  741. n = int(_np.dtype(len_t).type(next(fields)))
  742. data = _np.loadtxt(list(_islice(fields, n)), val_t, ndmin=1)
  743. if len(data) < n:
  744. raise StopIteration
  745. return data
  746. def _to_fields(self, data):
  747. '''
  748. Return generator over the (numerical) PLY representation of the
  749. list data (length followed by actual data).
  750. '''
  751. (len_t, val_t) = self.list_dtype()
  752. data = _np.asarray(data, dtype=val_t).ravel()
  753. yield _np.dtype(len_t).type(data.size)
  754. for x in data:
  755. yield x
  756. def _read_bin(self, stream, byte_order):
  757. (len_t, val_t) = self.list_dtype(byte_order)
  758. try:
  759. n = _read_array(stream, _np.dtype(len_t), 1)[0]
  760. except IndexError:
  761. raise StopIteration
  762. data = _read_array(stream, _np.dtype(val_t), n)
  763. if len(data) < n:
  764. raise StopIteration
  765. return data
  766. def _write_bin(self, data, stream, byte_order):
  767. '''
  768. Write data to a binary stream.
  769. '''
  770. (len_t, val_t) = self.list_dtype(byte_order)
  771. data = _np.asarray(data, dtype=val_t).ravel()
  772. _write_array(stream, _np.array(data.size, dtype=len_t))
  773. _write_array(stream, data)
  774. def __str__(self):
  775. len_str = _data_type_reverse[self.len_dtype]
  776. val_str = _data_type_reverse[self.val_dtype]
  777. return 'property list %s %s %s' % (len_str, val_str, self.name)
  778. def __repr__(self):
  779. return ('PlyListProperty(%r, %r, %r)' %
  780. (self.name,
  781. _lookup_type(self.len_dtype),
  782. _lookup_type(self.val_dtype)))
  783. def _check_name(name):
  784. for char in name:
  785. if not 0 <= ord(char) < 128:
  786. raise ValueError("non-ASCII character in name %r" % name)
  787. if char.isspace():
  788. raise ValueError("space character(s) in name %r" % name)
  789. def _read_array(stream, dtype, n):
  790. try:
  791. size = int(_np.dtype(dtype).itemsize * n)
  792. return _np.frombuffer(stream.read(size), dtype)
  793. except Exception:
  794. raise StopIteration
  795. def _write_array(stream, array):
  796. stream.write(array.tobytes())
  797. def _can_mmap(stream):
  798. try:
  799. pos = stream.tell()
  800. try:
  801. _np.memmap(stream, 'u1', 'c')
  802. stream.seek(pos)
  803. return True
  804. except Exception as e:
  805. stream.seek(pos)
  806. return False
  807. except Exception as e:
  808. return False