def _init_raw(self): rawfile = Raw(self.filename) self._rawimage = np.array(rawfile.raw_image()) # TODO maybe find better way to get color filter array # through a lookup table? #self._colorfilter = np.array(rawfile.color_filter_array) self._colorfilter = np.array([['R', 'G'], ['G', 'B']])
def process_image(filename): fpath = os.path.basename(filename) try: img = Image.open(filename) except IOError as e: if e.message.startswith("cannot identify image file"): tiff_output_file = os.path.join(TMPDIR, fpath + ".tiff") if not os.path.exists(tiff_output_file): print "error: cached tiff for", fpath, "does not exist at", tiff_output_file return fpath, None try: img = Image.open(tiff_output_file) except IOError as e3: print "Failed to open cached tiff", fpath, str(e3) return fpath, None else: print "IOError opening", filename, e return fpath, None format_ = img.format width = img.width height = img.height if width == 160: try: r = Raw(filename) except libraw.errors.FileUnsupported: print "libraw failed to read:", filename return fpath, None width = r.metadata.width height = r.metadata.height format_ = "tiff_raw" return filename, (format_, width, height)
def raw2png(input_dir, output_dir, alpha=0.5): not_extensions = [ 'png', 'jpg', 'jpeg', 'bmp', 'tif', 'tiff', 'gif', 'webp', 'svg' ] files = filter(lambda x: x.split('.')[-1].lower() not in not_extensions, os.listdir(input_dir)) for f in files: print f if not os.path.isfile(os.path.join(input_dir, f)): continue with Raw(filename=os.path.join(input_dir, f)) as raw: raw.options.interpolation = interpolation.ahd raw.options.white_balance = WhiteBalance(camera=False, auto=True) raw.options.auto_brightness = True raw.options.green_matching = True raw.options.rgbg_interpolation = False raw.options.auto_brightness_threshold = 0.01 raw.options.bps = 8 raw.save('/tmp/buffer.tiff') img = imread('/tmp/buffer.tiff', mode="RGB") clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(16, 16)) new = img.copy().astype(np.uint8) new[:, :, 0] = clahe.apply(img[:, :, 0]) new[:, :, 1] = clahe.apply(img[:, :, 1]) new[:, :, 2] = clahe.apply(img[:, :, 2]) new = img * alpha + (1 - alpha) * new imsave( os.path.join(output_dir, '.'.join(f.split('.')[:-1]) + '.png'), (new).astype(np.uint8))
def raw_thumbify(path): with Raw(path) as raw: path = Path(path) export = path.parent / 'thumb' thumb = str(export / path.name.replace('.dng', '_thumb.jpg')) raw.save_thumb(thumb) return thumb
def processFile(thread, path, temp, dest): """ Process the file""" logger.debug( "Workder {} Reading file {} with rawkit and converting to intermediate tiff" .format(thread, path)) # Convert Image from RAW to TIFF with Raw(path) as raw: raw.options.white_balance = WhiteBalance(camera=False, auto=True) raw.options.noise_threshold = 0.5 raw.save(filename=temp) logger.debug("Writing file to destination {} ".format(temp)) logger.debug( "Worker {} pplying additional filters and converting to JPEG {}". format(thread, dest)) # Use python vips try: if os.path.exists(temp): image = pyvips.Image.new_from_file(temp, access='sequential') mask = pyvips.Image.new_from_array( [[-1, -1, -1], [-1, 16, -1], [-1, -1, -1]], scale=8) image = image.conv(mask, precision='integer') image.write_to_file(dest) os.unlink(temp) except Exception as e: logger.error("Error converting tiff {} to jpeg with error {}".format( path, str(e)))
def main(): args = get_arguments() metadata = _extract_exif_metadata(args.filename) print "EXIF metadata:", metadata try: img = Image.open(args.filename) format_ = img.format if format_ == 'TIFF': output_file = "/tmp/" + filename + ".jpg" img.save(output_file) except IOError as e: try: with Raw(filename=fpath) as raw: tiff_output_file = "/tmp/" + filename + ".tiff" raw.save(filename=tiff_output_file) except Exception as e: logging.exception("Failed to parse file with PIL or rawkit: %s" % fpath) return False, fpath jpg_output_file = "/tmp/" + filename + ".jpg" img = Image.open(tiff_output_file) img.save(jpg_output_file) format_ = 'raw' print "Format:", format_ width = img.width height = img.height metadata = _extract_image_metadata(args.filename, format_, width, height, None) print "Width:", width print "Height:", height print "Image metadata:", metadata
def load_raw(raw_file_name): """ Loading and pre-processing the RAW image """ raw_image = Raw(filename=raw_file_name) raw_image.options.interpolation = interpolation.linear # n.b. "linear" and "amaze" have the same effect npimage = np.array(raw_image.raw_image(include_margin=False), dtype=float) # returns: 2D np. array ## vertical convolution to reduce noise (taking advantage of the multi-megapixel image) #vertical_averaging_kernel = np.outer(np.e**(-np.linspace(-1,1,vertical_convolution_length_px)**2),np.array([1])) vertical_averaging_kernel = np.outer( np.sin(-np.linspace(0, np.pi, vertical_convolution_length_px)**.5), np.array([1])) npimage = ndimage.convolve( npimage, vertical_averaging_kernel / np.sum(vertical_averaging_kernel)) ## optional: decimate data for faster processing npimage = ndimage.convolve( npimage, np.ones([decimate_factor, decimate_factor]) / decimate_factor**2) npimage = npimage[::decimate_factor, ::decimate_factor] ## optional: subtract constant background #TODO should subtract known "black frame" npimage -= np.min(npimage) return npimage
def to_cv2(filename): with Raw(filename=filename) as raw: raw.options.rotation = 0 w = raw.data.contents.sizes.width h = raw.data.contents.sizes.height na = np.frombuffer(raw.to_buffer(), np.int8) na = na.reshape(h, w, 3).astype("uint8") return cv2.cvtColor(na, cv2.COLOR_BGR2RGB)
def raw_to_jpg(file): try: with Raw(file) as raw_image: buffered_image = np.array(raw_image.to_buffer()) im = Image.frombuffer('RGB', (raw_image.metadata.width, raw_image.metadata.height), buffered_image, 'raw', 'RGB', 0, 1) im.save(os.path.split(file)[1] + '.jpg') print('Successfully saved file as JPG.') except OSError: print('Invalid file or location. Check your data. File: %s' % file)
def loadImage(self): loader = QtGui.QImage() loader.load(self.image_dir + '/' + self.main_window.surface_path + '.jpg') map = QtGui.QPixmap(loader) scaled_map = map.scaled(400, 400, QtCore.Qt.KeepAspectRatio) self.surface_label.setPixmap(scaled_map) loader.load(self.image_dir + '/' + self.main_window.scatter_path + '.jpg') map = QtGui.QPixmap(loader) scaled_map = map.scaled(400, 400, QtCore.Qt.KeepAspectRatio) self.scatter_label.setPixmap(scaled_map) # How long does metadata take # Metat data is a namedtuple container with Raw(filename=self.image_dir + '/' + self.main_window.surface_path + '.nef') as raw: self.iso_loaded_surface = raw.metadata.iso self.ss_loaded_surface = raw.metadata.shutter self.fstop_loaded_surface = np.around(raw.metadata.aperture, 1) # Convert the shutter speed to a number # self.ss_loaded_surface = int(np.ceil(1 / self.ss_loaded_surface)) # self.validateSurfaceImage() with Raw(filename=self.image_dir + '/' + self.main_window.scatter_path + '.nef') as raw: self.iso_loaded_scatter = raw.metadata.iso self.ss_loaded_scatter = raw.metadata.shutter self.fstop_loaded_scatter = np.around(raw.metadata.aperture, 1) # Convert the shutter speed to a number # self.ss_loaded_scatter = int(np.ceil(1 / self.ss_loaded_scatter)) # self.validateScatterImage() self.iso_label.setText( "ISO: " + str([self.iso_loaded_surface, self.iso_loaded_scatter])) self.fstop_label.setText( "FStop: " + str([self.fstop_loaded_surface, self.fstop_loaded_scatter])) self.ss_label.setText( "SS: " + str([self.ss_loaded_surface, self.ss_loaded_scatter]))
def from_raw(cls, path): with Raw(path) as raw: return cls( path, timestamp=raw.metadata.timestamp, camera_make=raw.metadata.make.decode("utf8"), camera_model=raw.metadata.model.decode("utf8"), aperture=round(raw.metadata.aperture, 1), focal_length=round(raw.metadata.focal_length), iso=round(raw.metadata.iso), )
def isRaw(f): try: Raw(f) return True except Exception as e: if type(e) is libraw.errors.FileUnsupported: print("File unsupported:", f) else: print("Error:", e) print("Ignoring this file.") return False
def load_raw_file_to_image(filename): """ Load a Raw file :param filename: :return: PIL.Image """ with Raw(filename=filename) as raw: raw.options.rotation = 0 w = raw.data.contents.sizes.width h = raw.data.contents.sizes.height buffered_image = np.array(raw.to_buffer()) image = Image.frombytes('RGB', (w, h), buffered_image) return image
def process_file(file): """ This function takes a file name of a raw image file with an extenstion .cr2 and saves a converted image in the defined save path povided to this script. The funcion will also verify the md5 hash if required Args: :param (File): Path to the file Returns: :return: File name of processed image or an error message Note: Function specifically coded to work with images form the Replica project. Error messages can be traced in the log file """ try: if file.verify_md5: if not md5.check_md5(file.path, file.path.replace(".cr2", ".md5")): f.write("{0} invalid md5\n".format(file)) f.flush() return "{0} invalid md5".format(file) raw_image = Raw(file.path) buffered_image = np.array(raw_image.to_buffer()) if (file.path.endswith("verso.cr2")): image = Image.frombytes( 'RGB', (raw_image.metadata.width, raw_image.metadata.height), buffered_image) image.save(file.save_path.replace(".cr2", ".jpg"), format='jpeg') f.write("{0}\n".format(file)) f.flush() return file else: image = Image.frombytes( 'RGB', (raw_image.metadata.width, raw_image.metadata.height), buffered_image) image.save(file.save_path.replace(".cr2", ".jpg"), format='jpeg') f.write("{0}\n".format(file)) f.flush() return file except Exception as e: f.write("{0} excepted with error: {1}\n".format(file, str(e)).format(file)) f.flush() return "{0} excepted with error: {1}".format(file, str(e))
def process_file(file: File): """ This function takes a file name of a raw image file with an extenstion .cr2 and saves a converted image in the defined save path povided to this script. The funcion will also verify the md5 hash if required Args: :param (File): Path to the file Returns: :return: File name of processed image or an error message Note: Function specifically coded to work with images form the Replica project. Error messages can be traced in the log file """ try: if os.path.exists(file.save_path) and skip_processed: return file if file.verify_md5: if md5.check_md5(file.path, file.path.replace(".cr2", ".md5")): logger.debug("{0} valid md5".format(file.path)) else: logger.error("{0} invalid md5".format(file.path)) with Raw(file.path) as raw_image: buffered_image = np.array(raw_image.to_buffer()) os.makedirs(os.path.dirname(file.save_path), exist_ok=True) buffered_image = buffered_image.reshape( (raw_image.metadata.height, raw_image.metadata.width, 3)) if 'verso' in file.path: buffered_image = buffered_image.reshape( (raw_image.metadata.width, raw_image.metadata.height, 3)) buffered_image = np.rot90(buffered_image, k=-1) else: buffered_image = buffered_image.reshape( (raw_image.metadata.height, raw_image.metadata.width, 3)) image = Image.fromarray(buffered_image) # image = Image.frombytes('RGB', (raw_image.metadata.height, raw_image.metadata.width), buffered_image) image.save(file.save_path, format='jpeg', quality=90) logger.info("Done processing {0}".format(file.path)) except Exception as e: logger.exception("{} excepted with error".format(file.path))
def Raw_Fits(self, raw_path, save_location): paths = [raw_path] bad_pixels = rawpy.enhance.find_bad_pixels(paths) for path in paths: with rawpy.imread(path) as raw: rawpy.enhance.repair_bad_pixels(raw, bad_pixels, method='median') rgb = raw.postprocess(no_auto_bright=True, use_auto_wb=False, gamma=None) a = np.array(rgb) print(a.shape) filename = raw_path raw_image = Raw(filename) buffered_image = np.array(raw_image.to_buffer()) image = Image.frombytes('RGB', (raw_image.metadata.width, raw_image.metadata.height), a).convert('LA') xsize, ysize = image.size data1 = np.array(image.getdata()) print(data1.shape) r = [(d[0]) for d in data1] g = [(d[1]) for d in data1] r_1 = np.array(r) g_1 = np.array(g) r_data = np.array(r_1.data) g_data = np.array(g_1.data) print(r_data.shape) r_data = r_data.reshape(ysize, xsize) g_data = g_data.reshape(ysize, xsize) a = cr2fits(raw_path, 0) b = cr2fits.read_exif(a) concat = r_data + g_data hdu = fits.PrimaryHDU(data=concat) hdu.header.set('OBSTIME', a.date) hdu.header.set('OBSTIME', a.date) hdu.header.set('EXPTIME', a.shutter) hdu.header.set('APERTUR', a.aperture) hdu.header.set('ISO', a.iso) hdu.header.set('FOCAL', a.focal) hdu.header.set('ORIGIN', a.original_file) hdu.header.set('FILTER', a.colors[a.colorInput]) hdu.header.set('CAMERA', a.camera) hdu.writeto(save_location, overwrite=True)
def __parseData(self, impath): # reads the metadata from the RAW file print("Reading file: " + impath) with Raw(impath) as img: idata = _demosaic(img.raw_image()) with open(impath, 'rb') as f: tags = exifread.process_file(f) exptime = float( Fraction(tags.get('EXIF ExposureTime').printable) ) dt = tags.get('EXIF DateTimeOriginal').printable (date, _, time) = dt.partition(' ') dt = tuple([int(i) for i in date.split(':') + time.split(':')]) dt = datetime(*dt).isoformat() #ofs = tags.get('EXIF TimeZoneOffset').printable jdate = Time(dt, format='isot', scale='utc').jd return idata, exptime, jdate
def process_auto(filename): """ Process a RAW image using libRAW with default settings. :param filename: input RAW image """ with Raw(filename=filename) as raw: raw.unpack() raw.process() image = raw.to_buffer() image = np.frombuffer(image, dtype=np.uint8) if raw.metadata.orientation == 5: image = image.reshape((raw.metadata.width, raw.metadata.height, 3)) else: image = image.reshape((raw.metadata.height, raw.metadata.width, 3)) return image
def getThumbnail(fl_input): print("imageProcessing::getThumbnail(fl_input)") try: # Define the output thumbnail file. fl_output = os.path.splitext(fl_input)[0] + ".thumb" + ".jpg" # Extract the thumbnail image from raw image file. with Raw(filename=fl_input) as img_raw: img_raw.save_thumb(filename=fl_output) # Return the result. return(fl_output) except Exception as e: print("Error occured in imageProcessing::getThumbnail(fl_input)") print(str(e)) error.ErrorMessageImageProcessing(details=str(e), show=True, language="en") return(None)
def develop(data, ext="png"): ext = (s for s in ext.split('.') if len(s)) ext = str(*ext) tempdir = mkdtemp() stemfn = str(uuid.uuid4()) stemfn = os.path.join(tempdir, stemfn) rawfn = stemfn + ".raw" intfn = stemfn + ".tiff" with open(rawfn, 'wb') as fh: fh.write(data) # raw to tiff raw = Raw(rawfn) raw.options.white_balance = WhiteBalance(camera=False, auto=True) raw.options.colorspace = colorspaces.adobe_rgb raw.options.gamma = gamma_curves.adobe_rgb raw.save(filename=intfn) # tiff to final format img = Image.open(intfn) new_img = io.BytesIO() img.save(new_img, format=ext) return new_img.getvalue()
def develop(self, cache_path=None): # develop photo developed_name = '{file_hash}.{extension}'.format( file_hash=self.file_hash, extension='ppm', ) developed_path = os.path.join( cache_path, 'ppm', developed_name, ) if not os.path.isfile(developed_path): with Raw(filename=self.raw_path) as raw: raw.options.half_size = True raw.save(filename=developed_path) photo = Photo.create(raw_path=self.raw_path, developed_path=developed_path, file_hash=self.file_hash) return photo
def api(): upload_to = app.config['UPLOAD_FOLDER'] if request.method == 'GET': path = request.args.get('path') media_host = app.config['MEDIA_HOST'] if path: media_host = media_host + path + '/' path = os.path.join(upload_to, path) else: path = upload_to files = os.listdir(path) paths = map(lambda x: os.path.join(path, x), files) return jsonify(**{'results': { 'media_host': media_host, 'paths': paths, 'files': files}}) elif request.method == 'PUT': image = request.files.get('image') if image and is_allowed(image.filename): metadata = get_metadata(image.stream) created = metadata['created'] created_path = get_path_by_created(created) filename = get_filename(secure_filename(image.filename), created) base_filename = os.path.splitext(filename)[0] outpath = get_outpath(filename, created_path) if 1: # check_outpath(outpath): create_directory(os.path.dirname(outpath)) image.save(outpath) export_path = os.path.join( upload_to, 'exports', created_path, base_filename + '.jpg') raw = Raw(filename=outpath) create_directory(os.path.dirname(export_path)) raw.save_thumb(export_path) return jsonify(**{'results': True}) return jsonify(**{'results': False})
def raw(input_file): with mock.patch('rawkit.raw.LibRaw'): with Raw(filename=input_file) as raw_obj: yield raw_obj raw_obj.libraw.libraw_close.assert_called_once_with(raw_obj.data)
import os.path import numpy as np #from astrotools.readers.readers import AstroImage PDIR = os.path.expanduser("~/pictures/17-03-18-custer-whirlpool") prefix = "IMG_" suffix = ".CR2" whirlpool_nums = np.arange(3432, 3530 + 1) darks = np.arange(3535, 3555 + 1) filename = PDIR + "/" + prefix + "3772" + suffix #filename = "/home/julienl/astropictures/170603-nick/IMG_9655.CR2" from rawkit.raw import Raw im = Raw(filename) raw = im.raw_image() print(raw)
def main(): logging.basicConfig( format='%(levelname)s: %(name)s: %(message)s', level=logging.WARNING) gp.check_result(gp.use_python_logging()) print('Establishing communication with the camera (wait few seconds)') camera = gp.check_result(gp.gp_camera_new()) gp.check_result(gp.gp_camera_init(camera)) # required configuration will depend on camera type! config = gp.check_result(gp.gp_camera_get_config(camera)) print('Camera ready') # TODO get the list of 'gp_abilities_list_get_abilities' or something like that ? # --> find out how to set image format # --> also use it to set shutter and ISO def my_set(name, value): OK, widget = gp.gp_widget_get_child_by_name(config, name) if OK >= gp.GP_OK: widget_type = gp.check_result(gp.gp_widget_get_type(widget)) gp.check_result(gp.gp_widget_set_value(widget, value)) gp.check_result(gp.gp_camera_set_config(camera, config)) else: print("Error setting value %s for %s using widget %s" % (value, name, widget)) #my_set(name='imageformat', value='Large Fine JPEG') my_set(name='imageformat', value='RAW') my_set(name='shutterspeed', value='{}'.format(shutterspeed)) my_set(name='iso', value='{}'.format(iso)) # find the image format config item OK, image_format = gp.gp_widget_get_child_by_name(config, 'imageformat') if OK >= gp.GP_OK: imgformat = gp.check_result(gp.gp_widget_get_value(image_format)) # find the capture size class config item # need to set this on my Canon 350d to get preview to work at all OK, capture_size_class = gp.gp_widget_get_child_by_name(config, 'capturesizeclass') if OK >= gp.GP_OK: # set value value = gp.check_result(gp.gp_widget_get_choice(capture_size_class, 2)) gp.check_result(gp.gp_widget_set_value(capture_size_class, value)) # set config gp.check_result(gp.gp_camera_set_config(camera, config)) # capture preview image (not saved to camera memory card) print('Capturing preview image') camera_file = gp.check_result(gp.gp_camera_capture_preview(camera)) file_data = gp.check_result(gp.gp_file_get_data_and_size(camera_file)) print('01----------', type(file_data),file_data) # display image data = memoryview(file_data) print('02----------', type(data), data) print(' ', len(data)) print(data[:10].tolist()) if 'raw' in imgformat.lower(): #rawimage = open(io.BytesIO(file_data)) ## FIXME raw format would be more accurate than JPEG, but AttributeError: '_io.BytesIO' object has no attribute 'encode' #xx from rawkit import raw #xx raw_image_process = raw.Raw(io.BytesIO(file_data)) #import rawpy #raw = rawpy.imread(bytesio) #rgb = raw.postprocess() #bytesio = io.BytesIO(file_data) #print('bytesio', bytesio) raw_file_name = 'image_logs/output_debayered_{}s_ISO{}_{}.cr2'.format(shutterspeed.replace('/','div'), iso, comment) gp.gp_file_save(camera_file, raw_file_name) # Note that - if Canon cameras are used - the dependency on rawkit can be replaced with a dedicated parser #https://codereview.stackexchange.com/questions/75374/cr2-raw-image-file-parser-in-python-3 from rawkit.raw import Raw from rawkit.options import interpolation raw_image = Raw(filename=raw_file_name) # 'bayer_data', 'close', 'color', 'color_description', 'color_filter_array', 'data', # 'image_unpacked', 'libraw', 'metadata', 'options', 'process', 'raw_image', 'save', # 'save_thumb', 'thumb_unpacked', 'thumbnail_to_buffer', 'to_buffer', 'unpack', 'unpack_thumb' #raw_image.options.interpolation = interpolation.linear # or "amaze", see https://rawkit.readthedocs.io/en/latest/api/rawkit.html raw_image.options.interpolation = interpolation.amaze # or "amaze", see https://rawkit.readthedocs.io/en/latest/api/rawkit.html raw_image.save("output-test.ppm") ## FIXME - saved ppm image has auto-normalized brightness, why? raw_image_process = raw_image.process() if raw_image_process is raw_image: print("they are identical") ## FIXME - npimage = np.array(raw_image.raw_image(include_margin=False)) # returns: 2D np. array #print('bayer_data', raw_image.bayer_data()) # ? #print('as_array', raw_image.as_array()) # does not exist, although documented?? #print(type(raw_image.to_buffer())) # Convert the image to an RGB buffer. Return type: bytearray #npimage = np.array(flat_list).reshape(4) print(npimage) ## gives 1-d array of values print(npimage.shape) ## gives 1-d array of values plt.imshow(npimage) plt.hist(npimage.flatten(), 4096) #plt.plot([200,500], [300,-100], lw=5, c='r') #plt.show() ## Save the raw pixels try: import cPickle as pickle except: import pickle print('retrieved image as numpy array with dimensions:', npimage.shape) #scipy.ndimage. print('', ) print('', ) print(npimage.shape) else: image = Image.open(io.BytesIO(file_data)) npimage = np.array(image) return npimage print('retrieved image as numpy array with dimensions:', npimage.shape) #image.show() plt.imshow(npimage) plt.plot([200,500], [300,-100], lw=5, c='k') plt.show() # TODO http://www.scipy-lectures.org/advanced/image_processing/#blurring-smoothing # display with polynomially curved paths, # linear convolve, # linearize along the paths, (possibly subtract background?) # generate polynomial x-axis # stitch smoothly gp.check_result(gp.gp_camera_exit(camera)) return 0
def render_photo_from_cr2_raw_file(original_filename, dest_filename): with Raw(filename=original_filename) as raw: raw.options.white_balance = WhiteBalance(camera=False, auto=True) raw.save(filename=dest_filename) return True
from rawkit.raw import Raw from rawkit.options import WhiteBalance with Raw(filename='original\\160119-3_nulevaya.cr2') as raw: raw.options.white_balance = WhiteBalance(camera=False, auto=True) raw.save(filename='image.ppm')
def test_create_no_filename(): with pytest.raises(NoFileSpecified): Raw()
#! /usr/bin/env python # Usage: tools/view_raw.py /path/to/raw/file import sys import matplotlib.pyplot as plt from rawkit.raw import Raw with Raw(filename=sys.argv[1]) as raw: plt.imshow(raw.as_array()) plt.show()
# Usage: python examples/save_jpeg_using_pil.py src.CR2 dest.jpg # Requires PIL (pip install pillow) import sys from PIL import Image from rawkit.raw import Raw src = sys.argv[1] dest = sys.argv[2] with Raw(filename=src) as raw: rgb_buffer = raw.to_buffer() # Convert the buffer from [r, g, b, r...] to [(r, g, b), (r, g, b)...] rgb_tuples = [ tuple(rgb_buffer[i:i + 3]) for i in range(0, len(rgb_buffer), 3) ] image = Image.new('RGB', [raw.metadata.width, raw.metadata.height]) image.putdata(rgb_tuples) image.save(dest)