paint-brush
如何创建应用程序来确定图像的调色板和主色经过@denissvinarchuk
406 讀數
406 讀數

如何创建应用程序来确定图像的调色板和主色

经过 Denis Svinarchuk18m2024/03/10
Read on Terminal Reader

太長; 讀書

图像编辑应用程序中的一项关键技术是确定主色。这些主色的识别对于创建图像的调色板至关重要,而调色板又可以准确地反映所选工具的效果和编辑结果。 本文将深入探讨如何在有限的色彩空间内确定图像的调色板、识别图像的主色以及区分调色板和主色。
featured image - 如何创建应用程序来确定图像的调色板和主色
Denis Svinarchuk HackerNoon profile picture

图像编辑应用程序中的一项关键技术是确定主色。这些主色的识别对于创建图像的调色板至关重要,而调色板又可以准确地反映所选工具的效果和编辑结果。


本文将深入探讨如何在有限的色彩空间内确定图像的调色板、识别图像的主色以及区分调色板和主色。

调色板

图像调色板通常是对原始图像中存在的所有颜色的引用。本质上,它根据颜色刺激信号值的数值范围捕获整个色调范围。


如果我们同意以一定的精度对信号进行建模,那么这些信号值的范围将代表我们可用的调色板。


图像的每个唯一表示以及图像颜色到该空间的映射都将是它的子集。在数字信号处理(图像也是信号)中,我们经常通过其离散表示来理解各种量。


因此,我们的图像调色板可以被视为由离散映射表示的图像中所有颜色的子集。可以对每种颜色进行索引并在其中一个颜色空间中为其分配特定值。出于我们的目的,我们将使用 RGB 颜色模型。


以这种方式呈现图像调色板的一个重大挑战是人类可见性的巨大性。我们不太可能手动分析整个图像调色板,并且存储由所有原始颜色表示的图像通常没有意义。相反,我们将其数量减少到合理的限度。


这一过程称为颜色量化,涉及将图像中可以表示的完整子集的颜色数量减少到较小的颜色数量。例如,14 位原始颜色数据可能以 8 位 JPEG 或转换为 8 位 PNG 的 16 位 TIFF 表示。


确定图像原色的一个直接解决方案是设想将颜色量化到非常有限的集合(例如 8 种甚至 3 种颜色)的过程。这使我们能够深入了解图像中的原色,使它们更加引人注目和令人难忘。


理想情况下,最终图像中的颜色应该是和谐的,遵循Johannes IttenMatyushin概述的原则。


通过识别图像的主色,我们可以创建一个工具来可视化图像的“和谐”。这也适用于滤波后图像的和谐,或“合成和谐”。


下面,我们将尝试开发这样一个工具。

调色板源立方直方图

中值切割

中值剪切算法是一种广泛使用的高质量压缩图像调色板的算法。它已被纳入大多数主要的有损图像压缩算法中,例如 JPEG,尽管进行了一些修改。


中值切割算法的基本原理是将图像的三次直方图依次划分为中值。每个生成的子立方体包含大约相同数量的箱或相同颜色的像素。


一旦划分过程达到预定的子立方体数量,我们就计算每个立方体的平均颜色并将其映射到原始图像的相应颜色。这个过程有效地减少了原始图像中的颜色数量,使我们能够将图像压缩为更小的文件大小。


乍一看,该算法的初始部分似乎可以解决我们识别图像的原色或主色的问题。然而,当我们将图像划分为的颜色数量太少并且我们只分析少量颜色时,就会出现一个潜在的问题。


我们最终可能会识别出图像中实际不存在的颜色,因为颜色过于平均。我们可以从每个立方体中选择具有最大容器的颜色并将其标记为主导颜色,但这样它就不再构成调色板了。


因此,我们将保留这种方法来确定图像调色板的压缩版本,它也可以用作根据图像的“和谐实用性”对图像进行可视化分析的工具。为了识别主色,我们将采用统计分析:在同一立方直方图中搜索局部最大值。

局部极大值

为了识别局部最大值,我们将实现特定的代码。作者也是一位熟练的艺术家,对该算法提供了精彩的描述。本质上,我们首先将图像统计数据收集到与中值切割算法中使用的相同的三维 RGB 直方图中。

三次直方图的每个单元格将包含颜色箱以及单元格中包含的每种颜色的所有值的总和。由于直方图尺寸有限,分辨率为 32x32x32(最初为 30x30x30),累加总和可简化平均单元格颜色的计算。

然后,我们通过彻底探索整个空间并将其与相邻单元进行比较来搜索局部最大值。

接下来,我们迭代地将局部最大值的数量减少到所需的数量,并丢弃权重较小的相似颜色。对于所有剩余的局部最大值,我们计算列表中包含的所有值的平均颜色。


由于局部最大值中的颜色密度较高,并且像素值之间的差异小于中值切割的立方体中的差异,因此颜色将更类似于主图像中存在的颜色,并且将更准确地表示其主色。


这揭示了两个模型之间的主要区别:获取“压缩调色板”与搜索局部最大值或主色。通过映射主要的“压缩调色板”图像,我们将创建一个新图像,该图像保持与主图像相同的色彩平衡,尽管形式被大幅截断。


另一方面,主色仅描述图像中主要存在的颜色的组成。它们无法转化为任何具有合适色彩平衡的新东西。

执行

以此任务为例,我们将演示使用IMProcessing Framework开发用于图像分析和操作的即用型应用程序是多么简单。我们将从其他引擎中没有的功能开始。


例如,该框架能够读取带有预先存在的 CLUT 的 Adob e .cube文件,并可以从图像中实时提取三维立方直方图。


利用这一点,我们的目标是创建一个能够:

  1. 上传 JFIF (jpeg) 格式的文件。


  2. “标准化”原始图像。


  3. 控制“标准化器”的强度。


  4. 将 Adobe .cube 文件中的任意 LUT 合并到处理中。


  5. 管理 CLUT 影响的强度。


  6. 显示图像的线性直方图。


  7. 以三元组 (r,g,b) 的形式展示图像的“压缩调色板”和主色及其数值表示


我们构建的最终产品将类似于这个互动玩具:

在这里,很明显图像的简单“标准化”如何对最终调色板的多样性产生积极影响,将主色重新分配为更加多样化和和谐的集合。

“规范化器”

我们将从两个预先存在的过滤器组装一个过滤器:

  1. IMPContrastFilter - 允许将图像直方图拉伸到指定的边界


  2. IMPAutoWBFilter - 基于搜索平均颜色和图像中杂散色调的校正来执行自动白平衡校正。这本质上是对从Andrey Zhuravlev 的博客中借用的一个想法的轻微修改。


 import IMProcessing /// Image filter public class IMPTestFilter:IMPFilter { /// We will use a contrast control filter through histogram stretching var contrastFilter:IMPContrastFilter! /// Auto white balance filter var awbFilter:IMPAutoWBFilter! /// Image Linear Histogram Analyzer var sourceAnalyzer:IMPHistogramAnalyzer! /// Solver of the histogram analyzer for calculating the lightness boundaries of the image let rangeSolver = IMPHistogramRangeSolver() public required init(context: IMPContext) { super.init(context: context) // Initialize filters in context contrastFilter = IMPContrastFilter(context: context) awbFilter = IMPAutoWBFilter(context: context) // Add filters to the stack addFilter(contrastFilter) addFilter(awbFilter) // Initialize the histogram analyzer sourceAnalyzer = IMPHistogramAnalyzer(context: self.context) // Add a light boundary search solver to the analyzer sourceAnalyzer.addSolver(rangeSolver) // Add an observing handler to the filter for // to pass the current image frame to the analyzer addSourceObserver { (source) - Void in self.sourceAnalyzer.source = source } // Add an observing handler for updating analysis calculations to the analyzer sourceAnalyzer.addUpdateObserver({ (histogram) - Void in // set the lightness boundaries in the contrast filter each time the image changes self.contrastFilter.adjustment.minimum = self.rangeSolver.minimum self.contrastFilter.adjustment.maximum = self.rangeSolver.maximum }) } }


调色板解算器

IMProcessing Framework 由于其独特的计算组织而从许多类似平台中脱颖而出。它使用一组专用的过滤器,这些过滤器本质上不是处理过滤器。


这些类的对象不会修改同质域和空间域中的图像。相反,他们在特殊扩展器中执行某些计算和指标表示以进行分析,以解决特定问题。


例如,IMPHistogramAnalyzer 类的对象可以同时添加多个求解器来计算图像的平均颜色、光照范围、区域划分等。


我们使用求解器扩展 IMPHistogramCubeAnalyzer 的分析来计算调色板和主色列表。然后计算结果显示在更新的 NSTableView 中。


 import IMProcessing /// Types of distribution of image color accents /// /// - palette: palette for quantizing image colors. /// calculated using the median-cut transformation scheme: /// http://www.leptonica.com/papers/mediancut.pdf /// - dominants: calculation of dominant colors of an image by searching for local maxima /// color distribution density functions: /// https://github.com/pixelogik/ColorCube /// public enum IMPPaletteType{ case palette case dominants } /// Solver of the cubic color histogram analyzer IMPHistogramCubeAnalyzer public class IMPPaletteSolver: IMPHistogramCubeSolver { /// Maximum number of palette colors for analysis public var maxColors = Int(8) /// List of found colors public var colors = [IMPColor]() /// Palette type public var type = IMPPaletteType.dominants /// Solver handler handler /// - parameter analyzer: link to the analyzer /// - parameter histogram: cubic histogram of the image /// - parameter imageSize: image size public func analizerDidUpdate(analizer: IMPHistogramCubeAnalyzer, histogram: IMPHistogramCube, imageSize: CGSize) { var p = [float3]() if type == .palette{ p = histogram.cube.palette(count: maxColors) } else if type == .dominants { p = histogram.cube.dominantColors(count: maxColors) } colors.removeAll() for c in p { colors.append(IMPColor(color: float4(rgb: c, a: 1))) } } }


我们在视图控制器中组装所有组件

控制器将需要主应用程序过滤器(我们将其称为IMPTestFilter ) 、名为 IMPLutFilter 的 CLUT 过滤器、用于显示“常规”直方图的即用型 IMPHistogramView、IMPHistogramCubeAnalyzer(我们将附加到的三次直方图分析器)我们的求解器 IMPPaletteSolver。


最后,我们将使用IMPImageView作为显示图像的主窗口,公共IMPContext是框架所有构造函数使用的关键类。


 class ViewController: NSViewController { // // Processing context // let context = IMPContext() // // Window for presenting the loaded image // var imageView:IMPImageView! var pannelScrollView = NSScrollView() // // Window for displaying the image histogram // var histogramView:IMPHistogramView! // // NSTableView - views of a list of colors from the palette // var paletteView:IMPPaletteListView! // // Main filter // var filter:IMPTestFilter! // // CLUT filter from Adobe Cube files // var lutFilter:IMPLutFilter? // // Analyzer of a cubic histogram of an image in RGB space // var histograCube:IMPHistogramCubeAnalyzer! // // Our solver for finding colors // var paletteSolver = IMPPaletteSolver() var paletteTypeChooser:NSSegmentedControl! override func viewDidLoad() { super.viewDidLoad() configurePannel() // // Initialize the objects we need // filter = IMPTestFilter(context: context) histograCube = IMPHistogramCubeAnalyzer(context: context) histograCube.addSolver(paletteSolver) imageView = IMPImageView(context: context, frame: view.bounds) imageView.filter = filter imageView.backgroundColor = IMPColor(color: IMPPrefs.colors.background) // // Add another handler to monitor the original image // (another one was added in the main filter IMPTestFilter) // filter.addSourceObserver { (source) -> Void in // // to minimize calculations, the analyzer will compress the image to 1000px on the wide side // if let size = source.texture?.size { let scale = 1000/max(size.width,size.height) self.histograCube.downScaleFactor = scale.float } } // Add an observer to the filter to process the filtering results // filter.addDestinationObserver { (destination) -> Void in // pass the image to the histogram indicator self.histogramView.source = destination // pass the result to the cubic histogram analyzer self.histograCube.source = destination } // // The results of updating the analyzer calculation are displayed in the color list window // histograCube.addUpdateObserver { (histogram) -> Void in self.asyncChanges({ () -> Void in self.paletteView.colorList = self.paletteSolver.colors }) } view.addSubview(imageView) .... IMPDocument.sharedInstance.addDocumentObserver { (file, type) -> Void in if type == .Image { do{ // // Load the file and associate it with the filter source // self.imageView.source = try IMPImageProvider(context: self.imageView.context, file: file) self.asyncChanges({ () -> Void in self.zoomFit() }) } catch let error as NSError { self.asyncChanges({ () -> Void in let alert = NSAlert(error: error) alert.runModal() }) } } else if type == .LUT { do { // // Initialize the CLUT descriptor // var description = IMPImageProvider.LutDescription() // // Load CLUT // let lutProvider = try IMPImageProvider(context: self.context, cubeFile: file, description: &description) if let lut = self.lutFilter{ // // If a CLUT filter has been added, update its LUT table from the file with the received descriptor // lut.update(lutProvider, description:description) } else{ // // Create a new LUT filter // self.lutFilter = IMPLutFilter(context: self.context, lut: lutProvider, description: description) } // // Add a LUT filter, if this filter has already been added nothing happens // self.filter.addFilter(self.lutFilter!) } catch let error as NSError { self.asyncChanges({ () -> Void in let alert = NSAlert(error: error) alert.runModal() }) } } } ....


正如您所看到的,照片处理变得越来越简单,对普通用户来说也更容易上手,几乎任何人都可以掌握这个过程。


整个项目可以从ImageMetalling项目存储库ImageMetalling-08下载、组装和测试。为了正确组装,必须在本地安装用于处理 JPEG 文件 (JFIF) 的大型库libjpeg-turbo


目前,这是对此格式支持的最佳实现。