diff --git a/src/ipa/libipa/bitdepth.h b/src/ipa/libipa/bitdepth.h
new file mode 100644
index 00000000..145ee093
--- /dev/null
+++ b/src/ipa/libipa/bitdepth.h
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Ideas On Board Oy.
+ *
+ * BitDepth class to abstract bit shift operations
+ */
+
+#pragma once
+
+template<unsigned int BitDepth>
+class BitDepthValue
+{
+public:
+	static_assert(BitDepth > 0, "Bit depth must be positive");
+
+	BitDepthValue()
+		: value_(0) {}
+
+	BitDepthValue(int value)
+		: value_(value) {}
+
+	BitDepthValue(unsigned int value)
+		: value_(value) {}
+
+	template<unsigned int TargetBitDepth>
+	BitDepthValue<TargetBitDepth> convert() const
+	{
+		static_assert(TargetBitDepth > 0, "Bit depth must be positive");
+
+		unsigned int shift;
+
+		if constexpr (BitDepth > TargetBitDepth) {
+			shift = BitDepth - TargetBitDepth;
+			return BitDepthValue<TargetBitDepth>(value_ >> shift);
+		} else if constexpr (BitDepth < TargetBitDepth) {
+			shift = TargetBitDepth - BitDepth;
+			return BitDepthValue<TargetBitDepth>(value_ << shift);
+		} else {
+			return BitDepthValue<TargetBitDepth>(value_);
+		}
+	}
+
+	unsigned int value() const
+	{
+		return value_;
+	}
+
+	template<unsigned int TargetBitDepth>
+	operator BitDepthValue<TargetBitDepth>() const
+	{
+		return convert<TargetBitDepth>();
+	}
+
+	operator unsigned int() const
+	{
+		return value_;
+	}
+
+private:
+	unsigned int value_;
+};
+
+inline BitDepthValue<8> operator"" _8bit(unsigned long long value)
+{
+	return BitDepthValue<8>(static_cast<unsigned int>(value));
+}
+
+inline BitDepthValue<10> operator"" _10bit(unsigned long long value)
+{
+	return BitDepthValue<10>(static_cast<unsigned int>(value));
+}
+
+inline BitDepthValue<12> operator"" _12bit(unsigned long long value)
+{
+	return BitDepthValue<12>(static_cast<unsigned int>(value));
+}
+
+inline BitDepthValue<14> operator"" _14bit(unsigned long long value)
+{
+	return BitDepthValue<14>(static_cast<unsigned int>(value));
+}
+
+inline BitDepthValue<16> operator"" _16bit(unsigned long long value)
+{
+	return BitDepthValue<16>(static_cast<unsigned int>(value));
+}
diff --git a/test/ipa/libipa/bitdepth.cpp b/test/ipa/libipa/bitdepth.cpp
new file mode 100644
index 00000000..d854637d
--- /dev/null
+++ b/test/ipa/libipa/bitdepth.cpp
@@ -0,0 +1,107 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Ideas On Board Oy.
+ *
+ * BitDepth converter tests
+ */
+
+#include "../../../src/ipa/libipa/bitdepth.h"
+
+#include <cmath>
+#include <iostream>
+#include <map>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <libcamera/base/log.h>
+
+#include "../../libtest/test.h"
+using namespace std;
+
+#define ASSERT_EQ(a, b)                    \
+	if ((a) != (b)) {                  \
+		printf(#a " != " #b "\n"); \
+		return TestFail;           \
+	}
+
+class BitDepthConverterTest : public Test
+{
+protected:
+	int run()
+	{
+		/* 10-bit to 16-bit conversion */
+		BitDepthValue<10> value10bit = 64_10bit;
+		BitDepthValue<16> value16bit = value10bit.convert<16>();
+		ASSERT_EQ(value16bit.value(), 4096);
+
+		/* Convert implicitly from another BitDepthValue */
+		value10bit = BitDepthValue<8>(16);
+		ASSERT_EQ(value10bit.value(), 64);
+		value10bit = 1_8bit;
+		ASSERT_EQ(value10bit.value(), 4);
+
+		/* Read value explicity and implicitly */
+		value10bit = BitDepthValue<10>(64);
+		ASSERT_EQ(value10bit.value(), 64);
+		ASSERT_EQ(value10bit, 64);
+
+		/* 12-bit to 8-bit conversion */
+		BitDepthValue<12> value12bit = 4096_12bit;
+		BitDepthValue<8> value8bit = value12bit.convert<8>();
+		ASSERT_EQ(value8bit.value(), 256);
+
+		/* Explicit bit depth assignment and conversion */
+		value16bit = 32768_16bit;
+		value12bit = value16bit.convert<12>();
+		ASSERT_EQ(value12bit.value(), 2048);
+
+		/* Test hex conversion */
+		value8bit = 0xFF_8bit;
+		ASSERT_EQ(value8bit, 255);
+
+		/* Test conversion to same bit depth makes no difference */
+		value16bit = 255_16bit;
+		value16bit = value16bit.convert<16>();
+		ASSERT_EQ(value16bit, 255);
+
+		/* Implicit bit depth assignment */
+		value12bit = 10;
+		ASSERT_EQ(value12bit.value(), 10);
+
+		/* 8-bit to 12-bit conversion */
+		value8bit = 128_8bit;
+		value12bit = value8bit.convert<12>();
+		ASSERT_EQ(value12bit.value(), 2048);
+
+		/* 16-bit to 8-bit conversion */
+		value16bit = 65535_16bit;
+		value8bit = value16bit.convert<8>();
+		ASSERT_EQ(value8bit.value(), 255);
+
+		/* Implicit assignment with int */
+		value8bit = 200;
+		ASSERT_EQ(value8bit.value(), 200);
+
+		/* 8-bit to 16-bit and back again */
+		value8bit = 150_8bit;
+		value16bit = value8bit.convert<16>();
+		value8bit = value16bit.convert<8>();
+		ASSERT_EQ(value8bit.value(), 150);
+
+		/* 12-bit to 16-bit and back again */
+		value12bit = 3000_12bit;
+		value16bit = value12bit.convert<16>();
+		ASSERT_EQ(value12bit, 3000);
+		ASSERT_EQ(value16bit, 48000)
+		value12bit = value16bit.convert<12>();
+		ASSERT_EQ(value12bit.value(), 3000);
+
+		/* Test negatives fail */
+		//value12bit = BitDepthValue<-1>;
+
+		return TestPass;
+	}
+};
+
+TEST_REGISTER(BitDepthConverterTest)
