UNPKG

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