// Copyright 2013 Lovell Fuller and others. // SPDX-License-Identifier: Apache-2.0 #include #include #include #include #include #include "common.h" #include "stats.h" class StatsWorker : public Napi::AsyncWorker { public: StatsWorker(Napi::Function callback, StatsBaton *baton, Napi::Function debuglog) : Napi::AsyncWorker(callback), baton(baton), debuglog(Napi::Persistent(debuglog)) {} ~StatsWorker() {} const int STAT_MIN_INDEX = 0; const int STAT_MAX_INDEX = 1; const int STAT_SUM_INDEX = 2; const int STAT_SQ_SUM_INDEX = 3; const int STAT_MEAN_INDEX = 4; const int STAT_STDEV_INDEX = 5; const int STAT_MINX_INDEX = 6; const int STAT_MINY_INDEX = 7; const int STAT_MAXX_INDEX = 8; const int STAT_MAXY_INDEX = 9; void Execute() { // Decrement queued task counter g_atomic_int_dec_and_test(&sharp::counterQueue); vips::VImage image; sharp::ImageType imageType = sharp::ImageType::UNKNOWN; try { std::tie(image, imageType) = OpenInput(baton->input); } catch (vips::VError const &err) { (baton->err).append(err.what()); } if (imageType != sharp::ImageType::UNKNOWN) { try { vips::VImage stats = image.stats(); int const bands = image.bands(); for (int b = 1; b <= bands; b++) { ChannelStats cStats( static_cast(stats.getpoint(STAT_MIN_INDEX, b).front()), static_cast(stats.getpoint(STAT_MAX_INDEX, b).front()), stats.getpoint(STAT_SUM_INDEX, b).front(), stats.getpoint(STAT_SQ_SUM_INDEX, b).front(), stats.getpoint(STAT_MEAN_INDEX, b).front(), stats.getpoint(STAT_STDEV_INDEX, b).front(), static_cast(stats.getpoint(STAT_MINX_INDEX, b).front()), static_cast(stats.getpoint(STAT_MINY_INDEX, b).front()), static_cast(stats.getpoint(STAT_MAXX_INDEX, b).front()), static_cast(stats.getpoint(STAT_MAXY_INDEX, b).front())); baton->channelStats.push_back(cStats); } // Image is not opaque when alpha layer is present and contains a non-mamixa value if (sharp::HasAlpha(image)) { double const minAlpha = static_cast(stats.getpoint(STAT_MIN_INDEX, bands).front()); if (minAlpha != sharp::MaximumImageAlpha(image.interpretation())) { baton->isOpaque = false; } } // Convert to greyscale vips::VImage greyscale = image.colourspace(VIPS_INTERPRETATION_B_W)[0]; // Estimate entropy via histogram of greyscale value frequency baton->entropy = std::abs(greyscale.hist_find().hist_entropy()); // Estimate sharpness via standard deviation of greyscale laplacian if (image.width() > 1 || image.height() > 1) { VImage laplacian = VImage::new_matrixv(3, 3, 0.0, 1.0, 0.0, 1.0, -4.0, 1.0, 0.0, 1.0, 0.0); laplacian.set("scale", 9.0); baton->sharpness = greyscale.conv(laplacian).deviate(); } // Most dominant sRGB colour via 4096-bin 3D histogram vips::VImage hist = sharp::RemoveAlpha(image) .colourspace(VIPS_INTERPRETATION_sRGB) .hist_find_ndim(VImage::option()->set("bins", 16)); std::complex maxpos = hist.maxpos(); int const dx = static_cast(std::real(maxpos)); int const dy = static_cast(std::imag(maxpos)); std::vector pel = hist(dx, dy); int const dz = std::distance(pel.begin(), std::find(pel.begin(), pel.end(), hist.max())); baton->dominantRed = dx * 16 + 8; baton->dominantGreen = dy * 16 + 8; baton->dominantBlue = dz * 16 + 8; } catch (vips::VError const &err) { (baton->err).append(err.what()); } } // Clean up vips_error_clear(); vips_thread_shutdown(); } void OnOK() { Napi::Env env = Env(); Napi::HandleScope scope(env); // Handle warnings std::string warning = sharp::VipsWarningPop(); while (!warning.empty()) { debuglog.MakeCallback(Receiver().Value(), { Napi::String::New(env, warning) }); warning = sharp::VipsWarningPop(); } if (baton->err.empty()) { // Stats Object Napi::Object info = Napi::Object::New(env); Napi::Array channels = Napi::Array::New(env); std::vector::iterator it; int i = 0; for (it = baton->channelStats.begin(); it < baton->channelStats.end(); it++, i++) { Napi::Object channelStat = Napi::Object::New(env); channelStat.Set("min", it->min); channelStat.Set("max", it->max); channelStat.Set("sum", it->sum); channelStat.Set("squaresSum", it->squaresSum); channelStat.Set("mean", it->mean); channelStat.Set("stdev", it->stdev); channelStat.Set("minX", it->minX); channelStat.Set("minY", it->minY); channelStat.Set("maxX", it->maxX); channelStat.Set("maxY", it->maxY); channels.Set(i, channelStat); } info.Set("channels", channels); info.Set("isOpaque", baton->isOpaque); info.Set("entropy", baton->entropy); info.Set("sharpness", baton->sharpness); Napi::Object dominant = Napi::Object::New(env); dominant.Set("r", baton->dominantRed); dominant.Set("g", baton->dominantGreen); dominant.Set("b", baton->dominantBlue); info.Set("dominant", dominant); Callback().MakeCallback(Receiver().Value(), { env.Null(), info }); } else { Callback().MakeCallback(Receiver().Value(), { Napi::Error::New(env, sharp::TrimEnd(baton->err)).Value() }); } delete baton->input; delete baton; } private: StatsBaton* baton; Napi::FunctionReference debuglog; }; /* stats(options, callback) */ Napi::Value stats(const Napi::CallbackInfo& info) { // V8 objects are converted to non-V8 types held in the baton struct StatsBaton *baton = new StatsBaton; Napi::Object options = info[size_t(0)].As(); // Input baton->input = sharp::CreateInputDescriptor(options.Get("input").As()); baton->input->access = VIPS_ACCESS_RANDOM; // Function to notify of libvips warnings Napi::Function debuglog = options.Get("debuglog").As(); // Join queue for worker thread Napi::Function callback = info[size_t(1)].As(); StatsWorker *worker = new StatsWorker(callback, baton, debuglog); worker->Receiver().Set("options", options); worker->Queue(); // Increment queued task counter g_atomic_int_inc(&sharp::counterQueue); return info.Env().Undefined(); }