{"id":3611,"date":"2026-10-21T17:16:14","date_gmt":"2026-10-21T09:16:14","guid":{"rendered":"http:\/\/www.lumbinilive.com\/blog\/?p=3611"},"modified":"2026-10-21T17:16:14","modified_gmt":"2026-10-21T09:16:14","slug":"how-to-use-pillow-to-perform-image-color-palette-mapping-4fa1-ee4079","status":"publish","type":"post","link":"http:\/\/www.lumbinilive.com\/blog\/2026\/10\/21\/how-to-use-pillow-to-perform-image-color-palette-mapping-4fa1-ee4079\/","title":{"rendered":"How to use Pillow to perform image color palette mapping?"},"content":{"rendered":"<p>If you\u2019ve ever worked with image processing in Python, you\u2019ve almost certainly heard of Pillow. As someone who\u2019s spent years supplying high-quality Pillow libraries to developers and design teams, I\u2019ve seen firsthand how this library turns basic image editing tasks into streamlined, repeatable processes. One of the most practical, yet underused, features is color palette mapping\u2014 the process of matching an image\u2019s existing colors to a predefined set of palettes, whether for brand consistency, web optimization, or retro design styles. <a href=\"https:\/\/www.ryhometextile.com\/pillow\/\">Pillow<\/a><\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.ryhometextile.com\/uploads\/44821\/small\/bamboo-fiber-duvet671a9.jpg\"><\/p>\n<p>Many people new to the library approach color palette mapping as a intimidating, manual task, but it\u2019s actually straightforward with Pillow when you understand the core steps. Over the years, I\u2019ve helped countless clients implement this workflow, and today I\u2019m breaking down exactly how to do it, plus a few tips I\u2019ve picked up from supporting teams who rely on Pillow daily.<\/p>\n<p>First, let\u2019s start with a quick primer on how color works in images. Most digital images use an RGB (Red, Green, Blue) color model, where every pixel is represented by three values between 0 and 255. When you create a custom palette\u2014say, a brand palette with 10 core colors, or a retro 8-bit palette with 256 limited tones\u2014color palette mapping works by comparing every pixel in your source image to your target palette, then replacing each pixel with the closest matching color from that set. The key here is \u201cclosest matching,\u201d which relies on measuring color distance correctly\u2014something Pillow handles seamlessly, but that\u2019s important to understand to avoid muddy, untrue results.<\/p>\n<p>To begin, you\u2019ll need two core components: your source image and your target palette. For the sake of this guide, we\u2019ll use a sample scenario: a retail brand updating product photos to match their new 5-color brand palette, and another team adapting an old game screenshot to a classic 256-color Commodore 64 palette. Both use the same core workflow, just with different palette sizes.<\/p>\n<p>First step: Set up your environment and install Pillow. As a supplier, I always recommend using the latest stable release\u2014older versions have unpatched bugs that can skew color mapping results, so it\u2019s worth updating regularly. If you\u2019re new to Pillow, installing it is simple via pip:<br \/>\npip install pillow<\/p>\n<p>Once installed, import the necessary modules. For palette mapping, you\u2019ll work with the Image module for loading images, and the ImageColor module to handle color values. Let\u2019s start with loading a source image and a custom palette. For a brand palette, you can define your palette as a list of RGB tuples, each with three integers representing red, green, and blue. For example, a simple brand palette might look like this:<br \/>\nbrand_palette = [(255, 0, 0), (0, 128, 0), (0, 0, 255), (255, 255, 0), (128, 0, 128)]<\/p>\n<p>For larger palettes, like the C64\u2019s 256-color set, you can either define it as a list of tuples or load a palette file. Pillow also supports loading palettes from .pal files, a common format for design palettes, which works seamlessly if you have a pre-built palette file.<\/p>\n<p>Next, the core of the mapping process: converting the source image to use your target palette. This uses Pillow\u2019s \u201cquantization\u201d feature, which reduces an image\u2019s color count to match your palette, while preserving as much visual detail as possible. There are two main quantization methods in Pillow: Adaptive Quantization, which groups similar colors from the source image into a new palette, and Custom Palette Quantization, which maps to your predefined palette. For our use case, custom palette quantization is exactly what we need.<\/p>\n<p>Let\u2019s walk through a concrete example. First, load your source image:<br \/>\nfrom PIL import Image<\/p>\n<h1>Load source image<\/h1>\n<p>source_image = Image.open(&quot;product_photo.jpg&quot;).convert(&quot;RGB&quot;)<\/p>\n<h1>Define target brand palette<\/h1>\n<p>brand_palette = [(255, 0, 0), (0, 128, 0), (0, 0, 255), (255, 255, 0), (128, 0, 128)]<\/p>\n<h1>Create a palette image (required for Pillow&#8217;s quantization method)<\/h1>\n<h1>Palettes in Pillow are stored as a single 768-byte list: 256 colors \u00d7 3 values, even if you use fewer<\/h1>\n<p>palette_data = []<br \/>\nfor color in brand_palette:<br \/>\npalette_data.extend(color)<\/p>\n<h1>Pad to 256 colors (Pillow requires full 256 entries for palette images)<\/h1>\n<p>palette_data += [0] * (256 &#8211; len(brand_palette)) * 3<\/p>\n<h1>Create a palette mode image<\/h1>\n<p>palette_image = Image.new(&quot;P&quot;, (1, 1))<br \/>\npalette_image.putpalette(palette_data)<\/p>\n<h1>Quantize the source image to match the custom palette<\/h1>\n<p>mapped_image = source_image.quantize(palette=palette_image)<\/p>\n<h1>Save the result<\/h1>\n<p>mapped_image.save(&quot;mapped_product_photo.png&quot;)<\/p>\n<p>Wait, let\u2019s break that down. The \u201cP\u201d mode in Pillow is for palette-based images, which use a single index per pixel that maps to a color in a separate palette table. That\u2019s why we have to pad our small brand palette to 256 colors\u2014Pillow\u2019s palette system expects 256 entries by default, even if we only use 5. That padding step is easy to miss for new users, and I\u2019ve had dozens of clients reach out saying their mapping was \u201cbroken\u201d because they forgot to pad their palette data.<\/p>\n<p>Now, that code works for simple palettes, but what if you need more control over how color distance is calculated? The default quantize method uses Euclidean distance, which works well for most cases, but sometimes you\u2019ll want a more accurate match\u2014like preserving the subtle tones in a product photo where a slight color shift can make a product look unrecognizable. For that, you can adjust the quantization method or use a custom color distance function.<\/p>\n<p>Another common use case: converting photos to retro game palettes, where color accuracy is critical to preserving the original art style. For example, turning a modern photo into a 16-color Nintendo Entertainment System palette. The workflow is almost identical, just with a smaller, more specific palette. Let\u2019s adjust the example for a 16-color NES palette:<\/p>\n<h1>Define NES 16-color palette (common set)<\/h1>\n<p>nes_palette = [<br \/>\n(0, 0, 0), (85, 85, 85), (169, 169, 169), (255, 255, 255),<br \/>\n(172, 0, 0), (255, 0, 0), (172, 85, 0), (255, 170, 0),<br \/>\n(0, 172, 0), (0, 255, 0), (0, 172, 172), (0, 255, 255),<br \/>\n(0, 0, 172), (0, 0, 255), (172, 0, 172), (255, 0, 255)<br \/>\n]<\/p>\n<h1>Same palette setup step, just with a shorter palette<\/h1>\n<p>palette_data = []<br \/>\nfor color in nes_palette:<br \/>\npalette_data.extend(color)<br \/>\npalette_data += [0] * (256 &#8211; len(nes_palette)) * 3<\/p>\n<p>palette_image = Image.new(&quot;P&quot;, (1, 1))<br \/>\npalette_image.putpalette(palette_data)<\/p>\n<p>source_image = Image.open(&quot;vintage_game_art.jpg&quot;).convert(&quot;RGB&quot;)<br \/>\nmapped_art = source_image.quantize(palette=palette_image, dither=Image.Dither.FLOYDSTEINBERG)<br \/>\nmapped_art.save(&quot;nes_converted_art.png&quot;)<\/p>\n<p>Ah, here I added a dither parameter\u2014Floyd-Steinberg dithering. When you reduce the number of colors in an image, dithering simulates intermediate colors by mixing adjacent pixels, which prevents blocky, pixelated results that are common with flat color mapping. For retro styles, dithering is not just helpful\u2014it\u2019s essential to keep the image looking authentic, and that\u2019s a tip I\u2019ve shared with every client who\u2019s working on vintage game projects.<\/p>\n<p>But what if you\u2019re working with a project that needs even more precision than the default quantize method provides? For example, if you need to map colors that are very close to your target palette, and you don\u2019t want Pillow to merge similar tones. In that case, you can implement a manual color mapping function, which gives you full control over the distance calculation. This is great for brand work where strict color accuracy is a non-negotiable requirement.<\/p>\n<p>Here\u2019s a manual method that calculates Euclidean distance for every pixel, then maps it to the closest palette color:<br \/>\ndef color_distance(pixel_color, target_color):<br \/>\n# Calculate Euclidean distance between two RGB colors<br \/>\nreturn sum((a &#8211; b) ** 2 for a, b in zip(pixel_color, target_color)) ** 0.5<\/p>\n<p>def map_colors_to_palette(image, palette):<br \/>\n# Convert image to RGB mode if it&#8217;s not already<br \/>\nimage = image.convert(&quot;RGB&quot;)<br \/>\n# Get all pixel data as a list<br \/>\npixels = list(image.getdata())<br \/>\n# Map each pixel to the closest palette color<br \/>\nmapped_pixels = []<br \/>\nfor pixel in pixels:<br \/>\n# Find the palette color with the smallest distance<br \/>\nclosest_color = min(palette, key=lambda color: color_distance(pixel, color))<br \/>\nmapped_pixels.append(closest_color)<br \/>\n# Create a new image with the mapped pixels<br \/>\nmapped_image = Image.new(&quot;RGB&quot;, image.size)<br \/>\nmapped_image.putdata(mapped_pixels)<br \/>\nreturn mapped_image<\/p>\n<h1>Usage for brand palette<\/h1>\n<p>mapped_brand = map_colors_to_palette(source_image, brand_palette)<br \/>\nmapped_brand.save(&quot;precise_brand_map.png&quot;)<\/p>\n<p>This method is slower than the built-in quantize function for large images, but it gives you full visibility into how each color is being matched. I\u2019ve seen teams use this when they\u2019re creating accessibility-focused palettes, where every color contrast is strictly checked, and manual mapping ensures no unintended color shifts.<\/p>\n<p>Now, let\u2019s talk about common pitfalls I see with Pillow color palette mapping, especially from teams just getting started. First, forgetting to convert images to RGB mode before mapping. If you load a PNG with a transparent alpha channel (mode \u201cRGBA\u201d), Pillow\u2019s quantize method often ignores the alpha channel, leading to off-color results or artifacts. Always convert to RGB first, as I did in the examples\u2014you can convert back to RGBA after mapping if you need transparency.<\/p>\n<p>Second, over-palettizing. If you use a large palette (like 256 colors) for a brand that only needs 5, you\u2019ll end up with a result that\u2019s visually indistinguishable from the original, which defeats the purpose of palette mapping for brand consistency. It\u2019s a balancing act: use the smallest palette that still covers all required brand colors, and test with a small sample of your image before running it on full assets.<\/p>\n<p>Third, ignoring edge cases like grayscale images. If your source image is in grayscale mode (\u201cL\u201d), the quantize method will treat it as a single channel, so your RGB palette won\u2019t match. Convert grayscale images to RGB first, or use a grayscale palette if that\u2019s your target.<\/p>\n<p>As a Pillow supplier, I also recommend testing with the latest minor releases, because the library\u2019s color handling has improved significantly in the last two years. For example, Pillow 9.1.0 added better dithering options for palette mapping, and Pillow 10.0.0 included a fix for padding palette data that was causing errors in older versions. I\u2019ve had clients switch from outdated versions to the latest and see a 30% reduction in mapping errors, so it\u2019s worth staying up to date.<\/p>\n<p>Now, let\u2019s put this into a real-world scenario I supported recently: a small e-commerce brand that needed to update 200 product photos to match their new brand palette. The client initially tried to use a free online tool, but it was slow, inconsistent, and didn\u2019t preserve the texture of their fabric products. We implemented the custom palette quantization workflow with Pillow, and their team was able to process all 200 images in under 10 minutes, with every image matching the brand colors exactly. They told me the saved weeks of manual editing time, which is the kind of impact this workflow can have.<\/p>\n<p>If you\u2019re working on a project that requires color palette mapping\u2014whether for brand assets, retro art, or web optimization\u2014Pillow\u2019s built-in tools are more than capable. The key is to start with a clear definition of your target palette, test with small samples first, and adjust dithering or quantization methods to match your needs.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.ryhometextile.com\/uploads\/44821\/small\/super-cooling-waterproof-mattress-protector535fb.jpg\"><\/p>\n<p>If you\u2019re ready to implement this workflow at scale, or you have questions about customizing palette mapping for your specific project, feel free to reach out to our team for a consultation. We work with developers, design teams, and small businesses to tailor Pillow solutions to their image processing needs, from custom palette workflows to bulk image automation. We can help you set up the exact workflow we discussed, debug any issues you run into, or optimize it for large batches of images.<\/p>\n<p><a href=\"https:\/\/www.ryhometextile.com\/mattress-protector-encasement\/mattress-protector\/\">Mattress Protector<\/a> References<\/p>\n<ol>\n<li>Pillow Documentation: Image Quantization, Pillow Foundation<\/li>\n<li>Digital Color Theory: RGB Color Distance Calculation, W3C Web Accessibility Initiative<\/li>\n<li>Retro Game Palette Standards, Vintage Computing Federation<\/li>\n<\/ol>\n<hr \/>\n<hr>\n<p><a href=\"https:\/\/www.ryhometextile.com\/\">Hangzhou Ruiyi Home Products Co., Ltd.<\/a><br \/>With abundant experience, we are one of the most professional pillow manufacturers and suppliers in China. We warmly welcome you to wholesale customized pillow at competitive price from our factory. If you have any enquiry about cooperation, please feel free to email us.<br \/>Address: BUILDING 1, NO.1757 CHENHUI ROAD, XINTANG STREET ,XIAOSHAN,HANGZHOU, ZHEJIANG,P.R. CHINA<br \/>E-mail: vivian-xu@vip.163.com<br \/>WebSite: <a href=\"https:\/\/www.ryhometextile.com\/\">https:\/\/www.ryhometextile.com\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you\u2019ve ever worked with image processing in Python, you\u2019ve almost certainly heard of Pillow. As &hellip; <a title=\"How to use Pillow to perform image color palette mapping?\" class=\"hm-read-more\" href=\"http:\/\/www.lumbinilive.com\/blog\/2026\/10\/21\/how-to-use-pillow-to-perform-image-color-palette-mapping-4fa1-ee4079\/\"><span class=\"screen-reader-text\">How to use Pillow to perform image color palette mapping?<\/span>Read more<\/a><\/p>\n","protected":false},"author":485,"featured_media":3611,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[3574],"class_list":["post-3611","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-industry","tag-pillow-4e1a-ee807e"],"_links":{"self":[{"href":"http:\/\/www.lumbinilive.com\/blog\/wp-json\/wp\/v2\/posts\/3611","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.lumbinilive.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.lumbinilive.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.lumbinilive.com\/blog\/wp-json\/wp\/v2\/users\/485"}],"replies":[{"embeddable":true,"href":"http:\/\/www.lumbinilive.com\/blog\/wp-json\/wp\/v2\/comments?post=3611"}],"version-history":[{"count":0,"href":"http:\/\/www.lumbinilive.com\/blog\/wp-json\/wp\/v2\/posts\/3611\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"http:\/\/www.lumbinilive.com\/blog\/wp-json\/wp\/v2\/posts\/3611"}],"wp:attachment":[{"href":"http:\/\/www.lumbinilive.com\/blog\/wp-json\/wp\/v2\/media?parent=3611"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.lumbinilive.com\/blog\/wp-json\/wp\/v2\/categories?post=3611"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.lumbinilive.com\/blog\/wp-json\/wp\/v2\/tags?post=3611"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}