You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

camera.py 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. # Copyright (c) 2016 Anki, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License in the file LICENSE.txt or at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. '''Support for Cozmo's camera.
  15. Cozmo has a built-in camera which he uses to observe the world around him.
  16. The :class:`Camera` class defined in this module is made available as
  17. :attr:`cozmo.world.World.camera` and can be used to enable/disable image
  18. sending, enable/disable color images, modify various camera settings,
  19. read the robot's unique camera calibration settings, as well as observe
  20. raw unprocessed images being sent by the robot.
  21. Generally, however, it is more useful to observe
  22. :class:`cozmo.world.EvtNewCameraImage` events, which include the raw camera
  23. images along with annotated images, which can illustrate objects the robot
  24. has identified.
  25. '''
  26. # __all__ should order by constants, event classes, other classes, functions.
  27. __all__ = ['EvtNewRawCameraImage', 'EvtRobotObservedMotion', 'CameraConfig', 'Camera']
  28. import functools
  29. import io
  30. _img_processing_available = True
  31. try:
  32. import numpy as np
  33. from PIL import Image
  34. except ImportError as exc:
  35. np = None
  36. _img_processing_available = exc
  37. from . import event
  38. from . import logger
  39. from . import util
  40. from ._clad import _clad_to_engine_iface, _clad_to_engine_cozmo, _clad_to_game_cozmo
  41. _clad_res = _clad_to_game_cozmo.ImageResolution
  42. RESOLUTIONS = {
  43. _clad_res.VerificationSnapshot: (16, 16),
  44. _clad_res.QQQQVGA: (40, 30),
  45. _clad_res.QQQVGA: (80, 60),
  46. _clad_res.QQVGA: (160, 120),
  47. _clad_res.QVGA: (320, 240),
  48. _clad_res.CVGA: (400, 296),
  49. _clad_res.VGA: (640, 480),
  50. _clad_res.SVGA: (800, 600),
  51. _clad_res.XGA: (1024, 768),
  52. _clad_res.SXGA: (1280, 960),
  53. _clad_res.UXGA: (1600, 1200),
  54. _clad_res.QXGA: (2048, 1536),
  55. _clad_res.QUXGA: (3200, 2400)
  56. }
  57. # wrap functions/methods that require NumPy or PIL with this
  58. # decorator to ensure they fail with a useful error if those packages
  59. # are not loaded.
  60. def _require_img_processing(f):
  61. @functools.wraps(f)
  62. def wrapper(*a, **kw):
  63. if _img_processing_available is not True:
  64. raise ImportError("Camera image processing not available: %s" % _img_processing_available)
  65. return f(*a, **kw)
  66. return wrapper
  67. class EvtNewRawCameraImage(event.Event):
  68. '''Dispatched when a new raw image is received from the robot's camera.
  69. See also :class:`~cozmo.world.EvtNewCameraImage` which provides access
  70. to both the raw image and a scaled and annotated version.
  71. '''
  72. image = 'A PIL.Image.Image object'
  73. class EvtRobotObservedMotion(event.Event):
  74. '''Generated when the robot observes motion.'''
  75. timestamp = "Robot timestamp for when movement was observed"
  76. img_area = "Area of the supporting region for the point, as a fraction of the image"
  77. img_pos = "Centroid of observed motion, relative to top-left corner"
  78. ground_area = "Area of the supporting region for the point, as a fraction of the ground ROI"
  79. ground_pos = "Approximate coordinates of observed motion on the ground, relative to robot, in mm"
  80. has_top_movement = "Movement detected near the top of the robot's view"
  81. top_img_pos = "Coordinates of the centroid of observed motion, relative to top-left corner"
  82. has_left_movement = "Movement detected near the left edge of the robot's view"
  83. left_img_pos = "Coordinates of the centroid of observed motion, relative to top-left corner"
  84. has_right_movement = "Movement detected near the right edge of the robot's view"
  85. right_img_pos = "Coordinates of the centroid of observed motion, relative to top-left corner"
  86. class CameraConfig:
  87. """The fixed properties for Cozmo's Camera
  88. A full 3x3 calibration matrix for doing 3D reasoning based on the camera
  89. images would look like:
  90. +--------------+--------------+---------------+
  91. |focal_length.x| 0 | center.x |
  92. +--------------+--------------+---------------+
  93. | 0 |focal_length.y| center.y |
  94. +--------------+--------------+---------------+
  95. | 0 | 0 | 1 |
  96. +--------------+--------------+---------------+
  97. """
  98. def __init__(self,
  99. focal_length_x: float,
  100. focal_length_y: float,
  101. center_x: float,
  102. center_y: float,
  103. fov_x_degrees: float,
  104. fov_y_degrees: float,
  105. min_exposure_time_ms: int,
  106. max_exposure_time_ms: int,
  107. min_gain: float,
  108. max_gain: float):
  109. self._focal_length = util.Vector2(focal_length_x, focal_length_y)
  110. self._center = util.Vector2(center_x, center_y)
  111. self._fov_x = util.degrees(fov_x_degrees)
  112. self._fov_y = util.degrees(fov_y_degrees)
  113. self._min_exposure_time_ms = min_exposure_time_ms
  114. self._max_exposure_time_ms = max_exposure_time_ms
  115. self._min_gain = min_gain
  116. self._max_gain = max_gain
  117. @classmethod
  118. def _create_from_clad(cls, cs):
  119. return cls(cs.focalLengthX, cs.focalLengthY,
  120. cs.centerX, cs.centerY,
  121. cs.fovX, cs.fovY,
  122. cs.minCameraExposureTime_ms, cs.maxCameraExposureTime_ms,
  123. cs.minCameraGain, cs.maxCameraGain)
  124. # Fixed camera properties (calibrated for each robot at the factory).
  125. @property
  126. def focal_length(self):
  127. ''':class:`cozmo.util.Vector2`: The focal length of the camera.
  128. This is focal length combined with pixel skew (as the pixels aren't
  129. perfectly square), so there are subtly different values for x and y.
  130. It is in floating point pixel values e.g. <288.87, 288.36>.
  131. '''
  132. return self._focal_length
  133. @property
  134. def center(self):
  135. ''':class:`cozmo.util.Vector2`: The focal center of the camera.
  136. This is the position of the optical center of projection within the
  137. image. It will be close to the center of the image, but adjusted based
  138. on the calibration of the lens at the factory. It is in floating point
  139. pixel values e.g. <155.11, 111.40>.
  140. '''
  141. return self._center
  142. @property
  143. def fov_x(self):
  144. ''':class:`cozmo.util.Angle`: The x (horizontal) field of view.'''
  145. return self._fov_x
  146. @property
  147. def fov_y(self):
  148. ''':class:`cozmo.util.Angle`: The y (vertical) field of view.'''
  149. return self._fov_y
  150. # The fixed range of values supported for this camera.
  151. @property
  152. def min_exposure_time_ms(self):
  153. '''int: The minimum supported exposure time in milliseconds.'''
  154. return self._min_exposure_time_ms
  155. @property
  156. def max_exposure_time_ms(self):
  157. '''int: The maximum supported exposure time in milliseconds.'''
  158. return self._max_exposure_time_ms
  159. @property
  160. def min_gain(self):
  161. '''float: The minimum supported camera gain.'''
  162. return self._min_gain
  163. @property
  164. def max_gain(self):
  165. '''float: The maximum supported camera gain.'''
  166. return self._max_gain
  167. class Camera(event.Dispatcher):
  168. '''Represents Cozmo's camera.
  169. The Camera object receives images from Cozmo's camera and emits
  170. EvtNewRawCameraImage events.
  171. The :class:`cozmo.world.World` instance observes the camera and provides
  172. more useful methods for accessing the camera images.
  173. .. important::
  174. The camera will not receive any image data unless you
  175. explicitly enable it by setting :attr:`Camera.image_stream_enabled`
  176. to ``True``
  177. '''
  178. def __init__(self, robot, **kw):
  179. super().__init__(**kw)
  180. self.robot = robot
  181. self._image_stream_enabled = None
  182. self._color_image_enabled = None
  183. self._config = None # type: CameraConfig
  184. self._gain = 0.0
  185. self._exposure_ms = 0
  186. self._auto_exposure_enabled = True
  187. if np is None:
  188. logger.warning("Camera image processing not available due to missing NumPy or Pillow packages: %s" % _img_processing_available)
  189. else:
  190. # set property to ensure clad initialization is sent.
  191. self.image_stream_enabled = False
  192. self.color_image_enabled = False
  193. self._reset_partial_state()
  194. def enable_auto_exposure(self, enable_auto_exposure = True):
  195. '''Enable auto exposure on Cozmo's Camera.
  196. Enable auto exposure on Cozmo's camera to constantly update the exposure
  197. time and gain values based on the recent images. This is the default mode
  198. when any SDK program starts.
  199. Args:
  200. enable_auto_exposure (bool): whether the camera should automcatically adjust exposure
  201. '''
  202. msg = _clad_to_engine_iface.SetCameraSettings(enableAutoExposure = enable_auto_exposure)
  203. self.robot.conn.send_msg(msg)
  204. def set_manual_exposure(self, exposure_ms, gain):
  205. '''Set manual exposure values for Cozmo's Camera.
  206. Disable auto exposure on Cozmo's camera and force the specified exposure
  207. time and gain values.
  208. Args:
  209. exposure_ms (int): The desired exposure time in milliseconds.
  210. Must be within the robot's
  211. :attr:`~cozmo.camera.Camera.config` exposure range from
  212. :attr:`~cozmo.camera.CameraConfig.min_exposure_time_ms` to
  213. :attr:`~cozmo.camera.CameraConfig.max_exposure_time_ms`
  214. gain (float): The desired gain value.
  215. Must be within the robot's
  216. :attr:`~cozmo.camera.Camera.camera_config` gain range from
  217. :attr:`~cozmo.camera.CameraConfig.min_gain` to
  218. :attr:`~cozmo.camera.CameraConfig.max_gain`
  219. Raises:
  220. :class:`ValueError` if supplied an out-of-range exposure or gain.
  221. '''
  222. cam = self.config
  223. if (exposure_ms < cam.min_exposure_time_ms) or (exposure_ms > cam.max_exposure_time_ms):
  224. raise ValueError('exposure_ms %s out of range %s..%s' %
  225. (exposure_ms, cam.min_exposure_time_ms, cam.max_exposure_time_ms))
  226. if (gain < cam.min_gain) or (gain > cam.max_gain):
  227. raise ValueError('gain %s out of range %s..%s' %
  228. (gain, cam.min_gain, cam.max_gain))
  229. msg = _clad_to_engine_iface.SetCameraSettings(enableAutoExposure=False,
  230. exposure_ms=exposure_ms,
  231. gain=gain)
  232. self.robot.conn.send_msg(msg)
  233. #### Private Methods ####
  234. def _reset_partial_state(self):
  235. self._partial_data = None
  236. self._partial_image_id = None
  237. self._partial_invalid = False
  238. self._partial_size = 0
  239. self._partial_metadata = None
  240. self._last_chunk_id = -1
  241. def _set_config(self, clad_config):
  242. self._config = CameraConfig._create_from_clad(clad_config)
  243. #### Properties ####
  244. @property
  245. @_require_img_processing
  246. def image_stream_enabled(self):
  247. '''bool: Set to true to receive camera images from the robot.'''
  248. if np is None:
  249. return False
  250. return self._image_stream_enabled
  251. @image_stream_enabled.setter
  252. @_require_img_processing
  253. def image_stream_enabled(self, enabled):
  254. if self._image_stream_enabled == enabled:
  255. return
  256. self._image_stream_enabled = enabled
  257. if enabled:
  258. image_send_mode = _clad_to_engine_cozmo.ImageSendMode.Stream
  259. else:
  260. image_send_mode = _clad_to_engine_cozmo.ImageSendMode.Off
  261. msg = _clad_to_engine_iface.ImageRequest(mode=image_send_mode)
  262. self.robot.conn.send_msg(msg)
  263. @property
  264. @_require_img_processing
  265. def color_image_enabled(self):
  266. '''bool: Set to true to receive color images from the robot.'''
  267. if np is None:
  268. return False
  269. return self._color_image_enabled
  270. @color_image_enabled.setter
  271. @_require_img_processing
  272. def color_image_enabled(self, enabled):
  273. if self._color_image_enabled == enabled:
  274. return
  275. self._color_image_enabled = enabled
  276. msg = _clad_to_engine_iface.EnableColorImages(enable = enabled)
  277. self.robot.conn.send_msg(msg)
  278. @property
  279. def config(self):
  280. ''':class:`cozmo.camera.CameraConfig`: The read-only config/calibration for the camera'''
  281. return self._config
  282. @property
  283. def is_auto_exposure_enabled(self):
  284. '''bool: True if auto exposure is currently enabled
  285. If auto exposure is enabled the `gain` and `exposure_ms`
  286. values will constantly be updated by Cozmo.
  287. '''
  288. return self._auto_exposure_enabled
  289. @property
  290. def gain(self):
  291. '''float: The current camera gain setting.'''
  292. return self._gain
  293. @property
  294. def exposure_ms(self):
  295. '''int: The current camera exposure setting in milliseconds.'''
  296. return self._exposure_ms
  297. #### Private Event Handlers ####
  298. def _recv_msg_image_chunk(self, evt, *, msg):
  299. if np is None:
  300. return
  301. if self._partial_image_id is not None and msg.chunkId == 0:
  302. if not self._partial_invalid:
  303. logger.debug("Lost final chunk of image; discarding")
  304. self._partial_image_id = None
  305. if self._partial_image_id is None:
  306. if msg.chunkId != 0:
  307. if not self._partial_invalid:
  308. logger.debug("Received chunk of broken image")
  309. self._partial_invalid = True
  310. return
  311. # discard any previous in-progress image
  312. self._reset_partial_state()
  313. self._partial_image_id = msg.imageId
  314. self._partial_metadata = msg
  315. max_size = msg.imageChunkCount * _clad_to_game_cozmo.ImageConstants.IMAGE_CHUNK_SIZE
  316. width, height = RESOLUTIONS[msg.resolution]
  317. max_size = width * height * 3 # 3 bytes (RGB) per pixel
  318. self._partial_data = np.empty(max_size, dtype=np.uint8)
  319. if msg.chunkId != (self._last_chunk_id + 1) or msg.imageId != self._partial_image_id:
  320. logger.debug("Image missing chunks; discarding (last_chunk_id=%d partial_image_id=%s)",
  321. self._last_chunk_id, self._partial_image_id)
  322. self._reset_partial_state()
  323. self._partial_invalid = True
  324. return
  325. offset = self._partial_size
  326. self._partial_data[offset:offset+len(msg.data)] = msg.data
  327. self._partial_size += len(msg.data)
  328. self._last_chunk_id = msg.chunkId
  329. if msg.chunkId == (msg.imageChunkCount - 1):
  330. self._process_completed_image()
  331. self._reset_partial_state()
  332. def _recv_msg_current_camera_params(self, evt, *, msg):
  333. self._gain = msg.cameraGain
  334. self._exposure_ms = msg.exposure_ms
  335. self._auto_exposure_enabled = msg.autoExposureEnabled
  336. def _recv_msg_robot_observed_motion(self, evt, *, msg):
  337. self.dispatch_event(EvtRobotObservedMotion,
  338. timestamp=msg.timestamp,
  339. img_area=msg.img_area,
  340. img_pos=util.Vector2(msg.img_x, msg.img_y),
  341. ground_area=msg.ground_area,
  342. ground_pos=util.Vector2(msg.ground_x, msg.ground_y),
  343. has_top_movement=(msg.top_img_area > 0),
  344. top_img_pos=util.Vector2(msg.top_img_x, msg.top_img_y),
  345. has_left_movement=(msg.left_img_area > 0),
  346. left_img_pos=util.Vector2(msg.left_img_x, msg.left_img_y),
  347. has_right_movement=(msg.right_img_area > 0),
  348. right_img_pos=util.Vector2(msg.right_img_x, msg.right_img_y))
  349. def _process_completed_image(self):
  350. data = self._partial_data[0:self._partial_size]
  351. # The first byte of the image is whether or not it is in color
  352. is_color_image = data[0] != 0
  353. if self._partial_metadata.imageEncoding == _clad_to_game_cozmo.ImageEncoding.JPEGMinimizedGray:
  354. width, height = RESOLUTIONS[self._partial_metadata.resolution]
  355. if is_color_image:
  356. # Color images are half width
  357. width = width // 2
  358. data = _minicolor_to_jpeg(data, width, height)
  359. else:
  360. data = _minigray_to_jpeg(data, width, height)
  361. image = Image.open(io.BytesIO(data)).convert('RGB')
  362. # Color images need to be resized to the proper resolution
  363. if is_color_image:
  364. size = RESOLUTIONS[self._partial_metadata.resolution]
  365. image = image.resize(size)
  366. self._latest_image = image
  367. self.dispatch_event(EvtNewRawCameraImage, image=image)
  368. #### Public Event Handlers ####
  369. @_require_img_processing
  370. def _minigray_to_jpeg(minigray, width, height):
  371. "Converts miniGrayToJpeg format to normal jpeg format"
  372. #This should be 'exactly' what is done in the miniGrayToJpeg function in encodedImage.cpp
  373. header50 = np.array([
  374. 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01,
  375. 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, #// 0x19 = QTable
  376. 0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23,
  377. 0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33, 0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40,
  378. 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67, 0x68, 0x67, 0x3E, 0x4D,
  379. #//0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0xF0, #// 0x5E = Height x Width
  380. 0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x01, 0x28, #// 0x5E = Height x Width
  381. #//0x01, 0x40, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0xD2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01,
  382. 0x01, 0x90, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0xD2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01,
  383. 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04,
  384. 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03,
  385. 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
  386. 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08,
  387. 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16,
  388. 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
  389. 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
  390. 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
  391. 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
  392. 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6,
  393. 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4,
  394. 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA,
  395. 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01,
  396. 0x00, 0x00, 0x3F, 0x00
  397. ], dtype=np.uint8)
  398. return _mini_to_jpeg_helper(minigray, width, height, header50)
  399. @_require_img_processing
  400. def _minicolor_to_jpeg(minicolor, width, height):
  401. "Converts miniColorToJpeg format to normal jpeg format"
  402. #This should be 'exactly' what is done in the miniColorToJpeg function in encodedImage.cpp
  403. header = np.array([
  404. 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01,
  405. 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, # 0x19 = QTable
  406. 0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23,
  407. 0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33, 0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40,
  408. 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67, 0x68, 0x67, 0x3E, 0x4D,
  409. 0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xC0, 0x00, 17, # 8+3*components
  410. 0x08, 0x00, 0xF0, # 0x5E = Height x Width
  411. 0x01, 0x40,
  412. 0x03, # 3 components
  413. 0x01, 0x21, 0x00, # Y 2x1 res
  414. 0x02, 0x11, 0x00, # Cb
  415. 0x03, 0x11, 0x00, # Cr
  416. 0xFF, 0xC4, 0x00, 0xD2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01,
  417. 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04,
  418. 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03,
  419. 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,
  420. 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08,
  421. 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16,
  422. 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
  423. 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
  424. 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
  425. 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
  426. 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6,
  427. 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4,
  428. 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA,
  429. 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA,
  430. 0xFF, 0xDA, 0x00, 12,
  431. 0x03, # 3 components
  432. 0x01, 0x00, # Y
  433. 0x02, 0x00, # Cb same AC/DC
  434. 0x03, 0x00, # Cr same AC/DC
  435. 0x00, 0x3F, 0x00
  436. ], dtype=np.uint8)
  437. return _mini_to_jpeg_helper(minicolor, width, height, header)
  438. @_require_img_processing
  439. def _mini_to_jpeg_helper(mini, width, height, header):
  440. bufferIn = mini.tolist()
  441. currLen = len(mini)
  442. headerLength = len(header)
  443. # For worst case expansion
  444. bufferOut = np.array([0] * (currLen*2 + headerLength), dtype=np.uint8)
  445. for i in range(headerLength):
  446. bufferOut[i] = header[i]
  447. bufferOut[0x5e] = height >> 8
  448. bufferOut[0x5f] = height & 0xff
  449. bufferOut[0x60] = width >> 8
  450. bufferOut[0x61] = width & 0xff
  451. # Remove padding at the end
  452. while (bufferIn[currLen-1] == 0xff):
  453. currLen -= 1
  454. off = headerLength
  455. for i in range(currLen-1):
  456. bufferOut[off] = bufferIn[i+1]
  457. off += 1
  458. if (bufferIn[i+1] == 0xff):
  459. bufferOut[off] = 0
  460. off += 1
  461. bufferOut[off] = 0xff
  462. off += 1
  463. bufferOut[off] = 0xD9
  464. bufferOut[:off]
  465. return np.asarray(bufferOut)