UNPKG

15 kBtext/x-cView Raw
1// Copyright 2013 Lovell Fuller and others.
2// SPDX-License-Identifier: Apache-2.0
3
4#include <algorithm>
5#include <functional>
6#include <memory>
7#include <tuple>
8#include <vector>
9#include <vips/vips8>
10
11#include "common.h"
12#include "operations.h"
13
14using vips::VImage;
15using vips::VError;
16
17namespace sharp {
18 /*
19 * Tint an image using the specified chroma, preserving the original image luminance
20 */
21 VImage Tint(VImage image, double const a, double const b) {
22 // Get original colourspace
23 VipsInterpretation typeBeforeTint = image.interpretation();
24 if (typeBeforeTint == VIPS_INTERPRETATION_RGB) {
25 typeBeforeTint = VIPS_INTERPRETATION_sRGB;
26 }
27 // Extract luminance
28 VImage luminance = image.colourspace(VIPS_INTERPRETATION_LAB)[0];
29 // Create the tinted version by combining the L from the original and the chroma from the tint
30 std::vector<double> chroma {a, b};
31 VImage tinted = luminance
32 .bandjoin(chroma)
33 .copy(VImage::option()->set("interpretation", VIPS_INTERPRETATION_LAB))
34 .colourspace(typeBeforeTint);
35 // Attach original alpha channel, if any
36 if (HasAlpha(image)) {
37 // Extract original alpha channel
38 VImage alpha = image[image.bands() - 1];
39 // Join alpha channel to normalised image
40 tinted = tinted.bandjoin(alpha);
41 }
42 return tinted;
43 }
44
45 /*
46 * Stretch luminance to cover full dynamic range.
47 */
48 VImage Normalise(VImage image, int const lower, int const upper) {
49 // Get original colourspace
50 VipsInterpretation typeBeforeNormalize = image.interpretation();
51 if (typeBeforeNormalize == VIPS_INTERPRETATION_RGB) {
52 typeBeforeNormalize = VIPS_INTERPRETATION_sRGB;
53 }
54 // Convert to LAB colourspace
55 VImage lab = image.colourspace(VIPS_INTERPRETATION_LAB);
56 // Extract luminance
57 VImage luminance = lab[0];
58
59 // Find luminance range
60 int const min = lower == 0 ? luminance.min() : luminance.percent(lower);
61 int const max = upper == 100 ? luminance.max() : luminance.percent(upper);
62
63 if (std::abs(max - min) > 1) {
64 // Extract chroma
65 VImage chroma = lab.extract_band(1, VImage::option()->set("n", 2));
66 // Calculate multiplication factor and addition
67 double f = 100.0 / (max - min);
68 double a = -(min * f);
69 // Scale luminance, join to chroma, convert back to original colourspace
70 VImage normalized = luminance.linear(f, a).bandjoin(chroma).colourspace(typeBeforeNormalize);
71 // Attach original alpha channel, if any
72 if (HasAlpha(image)) {
73 // Extract original alpha channel
74 VImage alpha = image[image.bands() - 1];
75 // Join alpha channel to normalised image
76 return normalized.bandjoin(alpha);
77 } else {
78 return normalized;
79 }
80 }
81 return image;
82 }
83
84 /*
85 * Contrast limiting adapative histogram equalization (CLAHE)
86 */
87 VImage Clahe(VImage image, int const width, int const height, int const maxSlope) {
88 return image.hist_local(width, height, VImage::option()->set("max_slope", maxSlope));
89 }
90
91 /*
92 * Gamma encoding/decoding
93 */
94 VImage Gamma(VImage image, double const exponent) {
95 if (HasAlpha(image)) {
96 // Separate alpha channel
97 VImage alpha = image[image.bands() - 1];
98 return RemoveAlpha(image).gamma(VImage::option()->set("exponent", exponent)).bandjoin(alpha);
99 } else {
100 return image.gamma(VImage::option()->set("exponent", exponent));
101 }
102 }
103
104 /*
105 * Flatten image to remove alpha channel
106 */
107 VImage Flatten(VImage image, std::vector<double> flattenBackground) {
108 double const multiplier = sharp::Is16Bit(image.interpretation()) ? 256.0 : 1.0;
109 std::vector<double> background {
110 flattenBackground[0] * multiplier,
111 flattenBackground[1] * multiplier,
112 flattenBackground[2] * multiplier
113 };
114 return image.flatten(VImage::option()->set("background", background));
115 }
116
117 /**
118 * Produce the "negative" of the image.
119 */
120 VImage Negate(VImage image, bool const negateAlpha) {
121 if (HasAlpha(image) && !negateAlpha) {
122 // Separate alpha channel
123 VImage alpha = image[image.bands() - 1];
124 return RemoveAlpha(image).invert().bandjoin(alpha);
125 } else {
126 return image.invert();
127 }
128 }
129
130 /*
131 * Gaussian blur. Use sigma of -1.0 for fast blur.
132 */
133 VImage Blur(VImage image, double const sigma) {
134 if (sigma == -1.0) {
135 // Fast, mild blur - averages neighbouring pixels
136 VImage blur = VImage::new_matrixv(3, 3,
137 1.0, 1.0, 1.0,
138 1.0, 1.0, 1.0,
139 1.0, 1.0, 1.0);
140 blur.set("scale", 9.0);
141 return image.conv(blur);
142 } else {
143 // Slower, accurate Gaussian blur
144 return image.gaussblur(sigma);
145 }
146 }
147
148 /*
149 * Convolution with a kernel.
150 */
151 VImage Convolve(VImage image, int const width, int const height,
152 double const scale, double const offset,
153 std::unique_ptr<double[]> const &kernel_v
154 ) {
155 VImage kernel = VImage::new_from_memory(
156 kernel_v.get(),
157 width * height * sizeof(double),
158 width,
159 height,
160 1,
161 VIPS_FORMAT_DOUBLE);
162 kernel.set("scale", scale);
163 kernel.set("offset", offset);
164
165 return image.conv(kernel);
166 }
167
168 /*
169 * Recomb with a Matrix of the given bands/channel size.
170 * Eg. RGB will be a 3x3 matrix.
171 */
172 VImage Recomb(VImage image, std::unique_ptr<double[]> const &matrix) {
173 double *m = matrix.get();
174 image = image.colourspace(VIPS_INTERPRETATION_sRGB);
175 return image
176 .recomb(image.bands() == 3
177 ? VImage::new_from_memory(
178 m, 9 * sizeof(double), 3, 3, 1, VIPS_FORMAT_DOUBLE
179 )
180 : VImage::new_matrixv(4, 4,
181 m[0], m[1], m[2], 0.0,
182 m[3], m[4], m[5], 0.0,
183 m[6], m[7], m[8], 0.0,
184 0.0, 0.0, 0.0, 1.0));
185 }
186
187 VImage Modulate(VImage image, double const brightness, double const saturation,
188 int const hue, double const lightness) {
189 VipsInterpretation colourspaceBeforeModulate = image.interpretation();
190 if (HasAlpha(image)) {
191 // Separate alpha channel
192 VImage alpha = image[image.bands() - 1];
193 return RemoveAlpha(image)
194 .colourspace(VIPS_INTERPRETATION_LCH)
195 .linear(
196 { brightness, saturation, 1},
197 { lightness, 0.0, static_cast<double>(hue) }
198 )
199 .colourspace(colourspaceBeforeModulate)
200 .bandjoin(alpha);
201 } else {
202 return image
203 .colourspace(VIPS_INTERPRETATION_LCH)
204 .linear(
205 { brightness, saturation, 1 },
206 { lightness, 0.0, static_cast<double>(hue) }
207 )
208 .colourspace(colourspaceBeforeModulate);
209 }
210 }
211
212 /*
213 * Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
214 */
215 VImage Sharpen(VImage image, double const sigma, double const m1, double const m2,
216 double const x1, double const y2, double const y3) {
217 if (sigma == -1.0) {
218 // Fast, mild sharpen
219 VImage sharpen = VImage::new_matrixv(3, 3,
220 -1.0, -1.0, -1.0,
221 -1.0, 32.0, -1.0,
222 -1.0, -1.0, -1.0);
223 sharpen.set("scale", 24.0);
224 return image.conv(sharpen);
225 } else {
226 // Slow, accurate sharpen in LAB colour space, with control over flat vs jagged areas
227 VipsInterpretation colourspaceBeforeSharpen = image.interpretation();
228 if (colourspaceBeforeSharpen == VIPS_INTERPRETATION_RGB) {
229 colourspaceBeforeSharpen = VIPS_INTERPRETATION_sRGB;
230 }
231 return image
232 .sharpen(VImage::option()
233 ->set("sigma", sigma)
234 ->set("m1", m1)
235 ->set("m2", m2)
236 ->set("x1", x1)
237 ->set("y2", y2)
238 ->set("y3", y3))
239 .colourspace(colourspaceBeforeSharpen);
240 }
241 }
242
243 VImage Threshold(VImage image, double const threshold, bool const thresholdGrayscale) {
244 if (!thresholdGrayscale) {
245 return image >= threshold;
246 }
247 return image.colourspace(VIPS_INTERPRETATION_B_W) >= threshold;
248 }
249
250 /*
251 Perform boolean/bitwise operation on image color channels - results in one channel image
252 */
253 VImage Bandbool(VImage image, VipsOperationBoolean const boolean) {
254 image = image.bandbool(boolean);
255 return image.copy(VImage::option()->set("interpretation", VIPS_INTERPRETATION_B_W));
256 }
257
258 /*
259 Perform bitwise boolean operation between images
260 */
261 VImage Boolean(VImage image, VImage imageR, VipsOperationBoolean const boolean) {
262 return image.boolean(imageR, boolean);
263 }
264
265 /*
266 Trim an image
267 */
268 VImage Trim(VImage image, std::vector<double> background, double threshold) {
269 if (image.width() < 3 && image.height() < 3) {
270 throw VError("Image to trim must be at least 3x3 pixels");
271 }
272 if (background.size() == 0) {
273 // Top-left pixel provides the default background colour if none is given
274 background = image.extract_area(0, 0, 1, 1)(0, 0);
275 } else if (sharp::Is16Bit(image.interpretation())) {
276 for (size_t i = 0; i < background.size(); i++) {
277 background[i] *= 256.0;
278 }
279 threshold *= 256.0;
280 }
281 std::vector<double> backgroundAlpha({ background.back() });
282 if (HasAlpha(image)) {
283 background.pop_back();
284 } else {
285 background.resize(image.bands());
286 }
287 int left, top, width, height;
288 left = image.find_trim(&top, &width, &height, VImage::option()
289 ->set("background", background)
290 ->set("threshold", threshold));
291 if (HasAlpha(image)) {
292 // Search alpha channel (A)
293 int leftA, topA, widthA, heightA;
294 VImage alpha = image[image.bands() - 1];
295 leftA = alpha.find_trim(&topA, &widthA, &heightA, VImage::option()
296 ->set("background", backgroundAlpha)
297 ->set("threshold", threshold));
298 if (widthA > 0 && heightA > 0) {
299 if (width > 0 && height > 0) {
300 // Combined bounding box (B)
301 int const leftB = std::min(left, leftA);
302 int const topB = std::min(top, topA);
303 int const widthB = std::max(left + width, leftA + widthA) - leftB;
304 int const heightB = std::max(top + height, topA + heightA) - topB;
305 return image.extract_area(leftB, topB, widthB, heightB);
306 } else {
307 // Use alpha only
308 return image.extract_area(leftA, topA, widthA, heightA);
309 }
310 }
311 }
312 if (width > 0 && height > 0) {
313 return image.extract_area(left, top, width, height);
314 }
315 return image;
316 }
317
318 /*
319 * Calculate (a * in + b)
320 */
321 VImage Linear(VImage image, std::vector<double> const a, std::vector<double> const b) {
322 size_t const bands = static_cast<size_t>(image.bands());
323 if (a.size() > bands) {
324 throw VError("Band expansion using linear is unsupported");
325 }
326 bool const uchar = !Is16Bit(image.interpretation());
327 if (HasAlpha(image) && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) {
328 // Separate alpha channel
329 VImage alpha = image[bands - 1];
330 return RemoveAlpha(image).linear(a, b, VImage::option()->set("uchar", uchar)).bandjoin(alpha);
331 } else {
332 return image.linear(a, b, VImage::option()->set("uchar", uchar));
333 }
334 }
335
336 /*
337 * Unflatten
338 */
339 VImage Unflatten(VImage image) {
340 if (HasAlpha(image)) {
341 VImage alpha = image[image.bands() - 1];
342 VImage noAlpha = RemoveAlpha(image);
343 return noAlpha.bandjoin(alpha & (noAlpha.colourspace(VIPS_INTERPRETATION_B_W) < 255));
344 } else {
345 return image.bandjoin(image.colourspace(VIPS_INTERPRETATION_B_W) < 255);
346 }
347 }
348
349 /*
350 * Ensure the image is in a given colourspace
351 */
352 VImage EnsureColourspace(VImage image, VipsInterpretation colourspace) {
353 if (colourspace != VIPS_INTERPRETATION_LAST && image.interpretation() != colourspace) {
354 image = image.colourspace(colourspace,
355 VImage::option()->set("source_space", image.interpretation()));
356 }
357 return image;
358 }
359
360 /*
361 * Split and crop each frame, reassemble, and update pageHeight.
362 */
363 VImage CropMultiPage(VImage image, int left, int top, int width, int height,
364 int nPages, int *pageHeight) {
365 if (top == 0 && height == *pageHeight) {
366 // Fast path; no need to adjust the height of the multi-page image
367 return image.extract_area(left, 0, width, image.height());
368 } else {
369 std::vector<VImage> pages;
370 pages.reserve(nPages);
371
372 // Split the image into cropped frames
373 for (int i = 0; i < nPages; i++) {
374 pages.push_back(
375 image.extract_area(left, *pageHeight * i + top, width, height));
376 }
377
378 // Reassemble the frames into a tall, thin image
379 VImage assembled = VImage::arrayjoin(pages,
380 VImage::option()->set("across", 1));
381
382 // Update the page height
383 *pageHeight = height;
384
385 return assembled;
386 }
387 }
388
389 /*
390 * Split into frames, embed each frame, reassemble, and update pageHeight.
391 */
392 VImage EmbedMultiPage(VImage image, int left, int top, int width, int height,
393 VipsExtend extendWith, std::vector<double> background, int nPages, int *pageHeight) {
394 if (top == 0 && height == *pageHeight) {
395 // Fast path; no need to adjust the height of the multi-page image
396 return image.embed(left, 0, width, image.height(), VImage::option()
397 ->set("extend", extendWith)
398 ->set("background", background));
399 } else if (left == 0 && width == image.width()) {
400 // Fast path; no need to adjust the width of the multi-page image
401 std::vector<VImage> pages;
402 pages.reserve(nPages);
403
404 // Rearrange the tall image into a vertical grid
405 image = image.grid(*pageHeight, nPages, 1);
406
407 // Do the embed on the wide image
408 image = image.embed(0, top, image.width(), height, VImage::option()
409 ->set("extend", extendWith)
410 ->set("background", background));
411
412 // Split the wide image into frames
413 for (int i = 0; i < nPages; i++) {
414 pages.push_back(
415 image.extract_area(width * i, 0, width, height));
416 }
417
418 // Reassemble the frames into a tall, thin image
419 VImage assembled = VImage::arrayjoin(pages,
420 VImage::option()->set("across", 1));
421
422 // Update the page height
423 *pageHeight = height;
424
425 return assembled;
426 } else {
427 std::vector<VImage> pages;
428 pages.reserve(nPages);
429
430 // Split the image into frames
431 for (int i = 0; i < nPages; i++) {
432 pages.push_back(
433 image.extract_area(0, *pageHeight * i, image.width(), *pageHeight));
434 }
435
436 // Embed each frame in the target size
437 for (int i = 0; i < nPages; i++) {
438 pages[i] = pages[i].embed(left, top, width, height, VImage::option()
439 ->set("extend", extendWith)
440 ->set("background", background));
441 }
442
443 // Reassemble the frames into a tall, thin image
444 VImage assembled = VImage::arrayjoin(pages,
445 VImage::option()->set("across", 1));
446
447 // Update the page height
448 *pageHeight = height;
449
450 return assembled;
451 }
452 }
453
454} // namespace sharp