From 72bf913f3d96a3260824361f9b410e32cc0d17e7 Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch
Date: Tue, 10 Oct 2023 09:31:13 +0200
Subject: Add a tool to find hot pixels
It works well for my Nikon.
Note that hot pixels can be eliminated in the camera itself,
when you run sensor cleaning immediately after a very long exposure
of darkness.
---
meson.build | 2 +-
tools/hotpixels.c | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 211 insertions(+), 1 deletion(-)
create mode 100644 tools/hotpixels.c
diff --git a/meson.build b/meson.build
index 10e12d7..9c616ad 100644
--- a/meson.build
+++ b/meson.build
@@ -192,7 +192,7 @@ if get_option('tools').enabled()
cc.find_library('jq'), dependency('libpng'), dependency('libraw')]
tools_c_args = cc.get_supported_arguments(
'-Wno-unused-function', '-Wno-unused-parameter')
- foreach tool : ['info', 'pnginfo', 'rawinfo']
+ foreach tool : ['info', 'pnginfo', 'rawinfo', 'hotpixels']
executable(tool, 'tools/' + tool + '.c', tiff_tables,
dependencies : tools_dependencies,
c_args: tools_c_args)
diff --git a/tools/hotpixels.c b/tools/hotpixels.c
new file mode 100644
index 0000000..ee1028c
--- /dev/null
+++ b/tools/hotpixels.c
@@ -0,0 +1,210 @@
+//
+// hotpixels.c: look for hot pixels in raw image files
+//
+// Usage: pass a bunch of raw photo images taken with the lens cap on at,
+// e.g., ISO 8000-12800 @ 1/20-1/60, and store the resulting file as,
+// e.g., Nikon D7500.badpixels, which can then be directly used by Rawtherapee.
+//
+// Copyright (c) 2023, Přemysl Eric Janouch
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+#include
+
+#if LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
+#error LibRaw 0.21.0 or newer is required.
+#endif
+
+#include
+#include
+#include
+#include
+#include
+
+static void *
+xreallocarray(void *o, size_t n, size_t m)
+{
+ if (m && n > SIZE_MAX / m) {
+ fprintf(stderr, "xreallocarray: %s\n", strerror(ENOMEM));
+ exit(EXIT_FAILURE);
+ }
+ void *p = realloc(o, n * m);
+ if (!p && n && m) {
+ fprintf(stderr, "xreallocarray: %s\n", strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+ return p;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct coord { ushort x, y; };
+
+static bool
+coord_equals(struct coord a, struct coord b)
+{
+ return a.x == b.x && a.y == b.y;
+}
+
+static int
+coord_cmp(const void *a, const void *b)
+{
+ const struct coord *ca = (const struct coord *) a;
+ const struct coord *cb = (const struct coord *) b;
+ return ca->y != cb->y
+ ? (int) ca->y - (int) cb->y
+ : (int) ca->x - (int) cb->x;
+}
+
+struct candidates {
+ struct coord *xy;
+ size_t len;
+ size_t alloc;
+};
+
+static void
+candidates_add(struct candidates *c, ushort x, ushort y)
+{
+ if (c->len == c->alloc) {
+ c->alloc += 64;
+ c->xy = xreallocarray(c->xy, sizeof *c->xy, c->alloc);
+ }
+
+ c->xy[c->len++] = (struct coord) {x, y};
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// A stretch of zeroes that is assumed to mean start of outliers.
+#define SPAN 10
+
+static const char *
+process_raw(struct candidates *c, const uint8_t *p, size_t len)
+{
+ libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
+ if (!iprc)
+ return "failed to obtain a LibRaw handle";
+
+ int err = 0;
+ if ((err = libraw_open_buffer(iprc, p, len)) ||
+ (err = libraw_unpack(iprc))) {
+ libraw_close(iprc);
+ return libraw_strerror(err);
+ }
+ if (!iprc->rawdata.raw_image) {
+ libraw_close(iprc);
+ return "only Bayer raws are supported, not Foveon";
+ }
+
+ // Make a histogram.
+ uint64_t bins[USHRT_MAX] = {};
+ for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
+ for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
+ ushort y = iprc->sizes.top_margin + yy;
+ ushort x = iprc->sizes.left_margin + xx;
+ bins[iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x]]++;
+ }
+ }
+
+ // Detecting outliers is not completely straight-forward,
+ // it may help to see the histogram.
+ if (getenv("HOTPIXELS_HISTOGRAM")) {
+ for (ushort i = 0; i < USHRT_MAX; i++)
+ fprintf(stderr, "%u ", (unsigned) bins[i]);
+ fputc('\n', stderr);
+ }
+
+ // Go to the first non-zero pixel value.
+ size_t last = 0;
+ for (; last < USHRT_MAX; last++)
+ if (bins[last])
+ break;
+
+ // Find the last pixel value we assume to not be hot.
+ for (; last < USHRT_MAX - SPAN - 1; last++) {
+ uint64_t nonzero = 0;
+ for (int i = 1; i <= SPAN; i++)
+ nonzero += bins[last + i];
+ if (!nonzero)
+ break;
+ }
+
+ // Store coordinates for all pixels above that value.
+ for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
+ for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
+ ushort y = iprc->sizes.top_margin + yy;
+ ushort x = iprc->sizes.left_margin + xx;
+ if (iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x] > last)
+ candidates_add(c, xx, yy);
+ }
+ }
+
+ libraw_close(iprc);
+ return NULL;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static const char *
+do_file(struct candidates *c, const char *filename)
+{
+ FILE *fp = fopen(filename, "rb");
+ if (!fp)
+ return strerror(errno);
+
+ uint8_t *data = NULL, buf[256 << 10];
+ size_t n, len = 0;
+ while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
+ data = xreallocarray(data, len + n, 1);
+ memcpy(data + len, buf, n);
+ len += n;
+ }
+
+ const char *err = ferror(fp)
+ ? strerror(errno)
+ : process_raw(c, data, len);
+
+ fclose(fp);
+ free(data);
+ return err;
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct candidates c = {};
+ for (int i = 1; i < argc; i++) {
+ const char *filename = argv[i], *err = do_file(&c, filename);
+ if (err) {
+ fprintf(stderr, "%s: %s\n", filename, err);
+ return EXIT_FAILURE;
+ }
+ }
+
+ qsort(c.xy, c.len, sizeof *c.xy, coord_cmp);
+
+ // If it is detected in all passed photos, it is probably indeed bad.
+ int count = 1;
+ for (size_t i = 1; i <= c.len; i++) {
+ if (i != c.len && coord_equals(c.xy[i - 1], c.xy[i])) {
+ count++;
+ continue;
+ }
+
+ if (count == argc - 1)
+ printf("%u %u\n", c.xy[i - 1].x, c.xy[i - 1].y);
+
+ count = 1;
+ }
+ return 0;
+}
--
cgit v1.2.3-70-g09d2