💙
PowerShellで色を RGB to HSV 変換する。
この記事は何?
- RGB 色空間で表現された色を HSV 色空間で表現された色に変換したいです。
- 言い換えると、いわゆる RGB to HSV 変換したい。
- ライブラリに依存せず、独自実装で変換したい。
- PowerShell で片づけましょう。
構成
OS, PowerShell, Pester のバージョンは次の通りです。
PS > (Get-WmiObject -Class Win32_OperatingSystem).Caption
Microsoft Windows 11 Home
PS > [string]$PSVersionTable.PSVersion
5.1.22000.653
PS > [string](Get-InstalledModule -Name Pester).Version
4.10.0
コード
HSVColorクラスを実装し、テストします。
ディレクトリ、ファイル構造
HSVColorクラスの実装があるファイル、テスト用のファイルの2つです。
PS > Get-ChildItem -Recurse | Select-Object -Property Name
Name
----
mycolor.ps1
mycolor.Tests.ps1
実装: mycolor.ps1
class IllegalArgumentException : Exception {
IllegalArgumentException([String]$Message) : base([String]$Message) {}
}
class InternalErrorException : Exception {
InternalErrorException([String]$Message) : base([String]$Message) {}
}
# RGB色空間の色
class RGBColor {
hidden [int32]$Red
hidden [int32]$Green
hidden [int32]$Blue
[int32]GetMaxPrimaryColorDepth() { return [Math]::Pow(2, 8) - 1 }
hidden ThrowExceptionIfPrimaryColorIsOutOfRange([int32]$PrimaryColor, [string]$ColorName) {
if (($PrimaryColor -lt 0) -or ($this.GetMaxPrimaryColorDepth() -lt $PrimaryColor)) {
$Message = "色 {0} が範囲 0-{2} の外です。: {1}" -f $ColorName, $PrimaryColor, $this.GetMaxPrimaryColorDepth()
throw [IllegalArgumentException]$Message
}
}
RGBColor([int32]$Red, [int32]$Green, [int32]$Blue) {
$this.ThrowExceptionIfPrimaryColorIsOutOfRange($Red, '赤')
$this.ThrowExceptionIfPrimaryColorIsOutOfRange($Green, '緑')
$this.ThrowExceptionIfPrimaryColorIsOutOfRange($Blue, '青')
$this.Red, $this.Green, $this.Blue = $Red, $Green, $Blue
}
static [RGBColor]FromRGB([int32]$Rgb) {
return [RGBColor]::new(($Rgb % 256), (($Rgb -shr 8) % 256), (($Rgb -shr 16) % 256))
}
[int32]GetRed() { return $this.Red }
[int32]GetGreen() { return $this.Green }
[int32]GetBlue() { return $this.Blue }
[int32]GetRGB() {
return ((($this.Blue -shl 8) + $this.Green) -shl 8) + $this.Red
}
}
# 角度
class AngleInDegree {
hidden [float]$AngleInDegree
AngleInDegree([float]$AngleInDegree) {
if (($AngleInDegree -lt 0) -or (360 -le $AngleInDegree)) {
$Message = '角度が範囲 0 以上 360 未満の外です。: {0}' -f $AngleInDegree
throw [IllegalArgumentException]$Message
}
$this.AngleInDegree = $AngleInDegree
}
[float]GetAngleInDegree() { return $this.AngleInDegree }
}
# HSV色空間の彩度
class HSVSaturation {
hidden [float]$Saturation
HSVSaturation([float]$Saturation) {
if (($Saturation -lt 0) -or (1 -lt $Saturation)) {
$Message = '彩度が範囲 0-1 の外です。: {0}' -f $Saturation
throw [IllegalArgumentException]$Message
}
$this.Saturation = $Saturation
}
[float]GetSaturation() { return $this.Saturation }
}
# HSV色空間の明度
class HSVValue {
hidden [float]$Value
HSVValue([float]$Value) {
if (($Value -lt 0) -or (1 -lt $Value)) {
$Message = '明度が範囲 0-1 の外です。: {0}' -f $Value
throw [IllegalArgumentException]$Message
}
$this.Value = $Value
}
[float]GetValue() { return $this.Value }
}
# HSV色空間の色
class HSVColor {
hidden [AngleInDegree]$Hue # 色相
hidden [HSVSaturation]$Saturation # 彩度
hidden [HSVValue]$Value # 明度
HSVColor([AngleInDegree]$Hue, [HSVSaturation]$Saturation, [HSVValue]$Value) {
$this.Hue = $Hue
$this.Saturation = $Saturation
$this.Value = $Value
}
[AngleInDegree]GetHue() { return $this.Hue }
[HSVSaturation]GetSaturation() { return $this.Saturation }
[HSVValue]GetValue() { return $this.Value }
[HSVValue]GetBrightness() { return $this.GetValue() }
static [HSVColor]FromRGBColor([RGBColor]$Rgb) {
# RGB を HSV に変換します。
# atan() などを使用せず、近似的に計算します。
# RGB を 0.0 - 1.0 の範囲に変換する。
[float]$Red = $Rgb.GetRed() / $Rgb.GetMaxPrimaryColorDepth()
[float]$Green = $Rgb.GetGreen() / $Rgb.GetMaxPrimaryColorDepth()
[float]$Blue = $Rgb.GetBlue() / $Rgb.GetMaxPrimaryColorDepth()
[float[]]$PrimaryColors = $Red, $Green, $Blue
# RGBの最大値、最小値を得る。
$RgbMax = [System.Linq.Enumerable]::Max($PrimaryColors)
$RgbMin = [System.Linq.Enumerable]::Min($PrimaryColors)
# Hue を計算する。
$HueValue = 0
if ($RgbMax -eq $Red) {
if ($RgbMax -ne $RgbMin) {
$HueValue = 60 * ($Green - $Blue) / ($RgbMax - $RgbMin)
if ($HueValue -lt 0) {
$HueValue += 360
}
}
} elseif ($RgbMax -eq $Green) {
if ($RgbMax -ne $RgbMin) {
$HueValue = 60 * ($Blue - $Red) / ($RgbMax - $RgbMin) + 120
}
} elseif ($RgbMax -eq $Blue) {
if ($RgbMax -ne $RgbMin) {
$HueValue = 60 * ($Red - $Green) / ($RgbMax - $RgbMin) + 240
}
} else {
throw [InternalErrorException]
}
$local:Hue = [AngleInDegree]::new($HueValue)
# 彩度を計算する。
$SaturationValue = 0
if ($RgbMax -ne 0) {
$SaturationValue = ($RgbMax - $RgbMin) / $RgbMax
}
$local:Saturation = [HSVSaturation]::new($SaturationValue)
# 明度を計算する。
$local:Value = [HSVValue]::new($RgbMax)
return [HSVColor]::new($local:Hue, $local:Saturation, $local:Value)
}
}
テスト: mycolor.Tests.ps1
各種の値は HSL and HSV を参照してください。
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
. "$here\$sut"
Describe "RGBColor" {
Context "正常" {
It "赤を返す。" {
$c = [RGBColor]::new(10, 20, 30)
$c.GetRed() | Should -Be 10
}
It "緑を返す。" {
$c = [RGBColor]::new(10, 20, 30)
$c.GetGreen() | Should -Be 20
}
It "青を返す。" {
$c = [RGBColor]::new(10, 20, 30)
$c.GetBlue() | Should -Be 30
}
It "すべて0で例外を発生しない。" {
[RGBColor]::new(0, 0, 0)
}
It "すべて255で例外を発生しない。" {
[RGBColor]::new(255, 255, 255)
}
}
Context "異常" {
It "色の要素が0未満ならば例外を発生する。" {
try {
[RGBColor]::new(-1, -1, -1)
$true | Should -Be $false
} catch {
}
}
It "色の要素が256以上ならば例外を発生する。" {
try {
[RGBColor]::new(256, 256, 256)
$true | Should -Be $false
} catch {
}
}
}
}
Describe "AngleInDegree" {
Context "正常" {
It "0度を返す。" {
$a = [AngleInDegree]::new(0)
$a.GetAngleInDegree() | Should -Be 0
}
It "359度を返す。" {
$a = [AngleInDegree]::new(359)
$a.GetAngleInDegree() | Should -Be 359
}
}
Context "異常" {
It "0未満ならば例外を発生する。" {
try {
[AngleInDegree]::new(-0.1)
$true | Should -Be $false
} catch {
}
}
It "360度以上ならば例外を発生する。" {
try {
[AngleInDegree]::new(360)
$true | Should -Be $false
} catch {
}
}
}
}
Describe "HSVSaturation" {
Context "正常" {
It "0を返す。" {
$s = [HSVSaturation]::new(0)
$s.GetSaturation() | Should -Be 0
}
It "1を返す。" {
$s = [HSVSaturation]::new(1)
$s.GetSaturation() | Should -Be 1
}
}
Context "異常" {
It "0未満ならば例外を発生する。" {
try {
[HSVSaturation]::new(-0.1)
$true | Should -Be $false
} catch {
}
}
It "1を超えるならば例外を発生する。" {
try {
[HSVSaturation]::new(1.1)
$true | Should -Be $false
} catch {
}
}
}
}
Describe "HSVValue" {
Context "正常" {
It "0を返す。" {
$s = [HSVValue]::new(0)
$s.GetValue() | Should -Be 0
}
It "1を返す。" {
$s = [HSVValue]::new(1)
$s.GetValue() | Should -Be 1
}
}
Context "異常" {
It "0未満ならば例外を発生する。" {
try {
[HSVValue]::new(-0.1)
$true | Should -Be $false
} catch {
}
}
It "1を超えるならば例外を発生する。" {
try {
[HSVValue]::new(1.1)
$true | Should -Be $false
} catch {
}
}
}
}
Describe "HSVColor" {
Context "RGB to HSV" {
Context "正常" {
It "色相0度を返す。" {
$RgbPct = 100, 0, 0
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
$Hue = $Color2.GetHue()
$Diff = [Math]::Abs($Hue.GetAngleInDegree() - 0)
$Diff | Should -BeLessThan 0.1
}
It "色相60度を返す。" {
$RgbPct = 75, 75, 0
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
$Hue = $Color2.GetHue()
$Diff = [Math]::Abs($Hue.GetAngleInDegree() - 60)
$Diff | Should -BeLessThan 0.1
}
It "色相120度を返す。" {
$RgbPct = 0, 50, 0
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
$Hue = $Color2.GetHue()
$Diff = [Math]::Abs($Hue.GetAngleInDegree() - 120)
$Diff | Should -BeLessThan 0.1
}
It "色相180度を返す。" {
$RgbPct = 50, 100, 100
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
$Hue = $Color2.GetHue()
$Diff = [Math]::Abs($Hue.GetAngleInDegree() - 180)
$Diff | Should -BeLessThan 0.1
}
It "色相240度を返す。" {
$RgbPct = 50, 50, 100
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
$Hue = $Color2.GetHue()
$Diff = [Math]::Abs($Hue.GetAngleInDegree() - 240)
$Diff | Should -BeLessThan 0.1
}
It "色相300度を返す。" {
$RgbPct = 75, 25, 75
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
$Hue = $Color2.GetHue()
$Diff = [Math]::Abs($Hue.GetAngleInDegree() - 300)
$Diff | Should -BeLessThan 0.1
}
It "彩度を返す。" {
$RgbPct = 75, 75, 0
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
$Saturation = $Color2.GetSaturation()
$Diff = [Math]::Abs($Saturation.GetSaturation() - 1)
$Diff | Should -BeLessThan 0.01
}
It "明度を返す。" {
$RgbPct = 75, 75, 0
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
$Value = $Color2.GetValue()
$Diff = [Math]::Abs($Value.GetValue() - 0.75)
$Diff | Should -BeLessThan 0.01
}
It "RGB(100%,100%,100%)は期待通りのHSVを返す。" {
$RgbPct = 100, 100, 100
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
# 白の色相は0を返す仕様とする。
$Hue = $Color2.GetHue()
$AngleInDegree = $Hue.GetAngleInDegree()
$Diff = [Math]::Abs($AngleInDegree - 0)
$Diff | Should -BeLessThan 0.1
$Saturation = $Color2.GetSaturation()
$Diff = [Math]::Abs($Saturation.GetSaturation() - 0)
$Diff | Should -BeLessThan 0.01
$Value = $Color2.GetValue()
$Diff = [Math]::Abs($Value.GetValue() - 1)
$Diff | Should -BeLessThan 0.01
}
It "RGB(0%,0%,0%)は期待通りのHSVを返す。" {
$RgbPct = 0, 0, 0
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
# 黒の色相は0を返す仕様とする。
$Hue = $Color2.GetHue()
$AngleInDegree = $Hue.GetAngleInDegree()
$Diff = [Math]::Abs($AngleInDegree - 0)
$Diff | Should -BeLessThan 0.1
$Saturation = $Color2.GetSaturation()
$Diff = [Math]::Abs($Saturation.GetSaturation() - 0)
$Diff | Should -BeLessThan 0.01
$Value = $Color2.GetValue()
$Diff = [Math]::Abs($Value.GetValue() - 0)
$Diff | Should -BeLessThan 0.01
}
It "RGB(62.8%,64.3%,14.2%)は期待通りのHSVを返す。" {
$RgbPct = 62.8, 64.3, 14.2
$Rgb = $RgbPct | ForEach-Object { ($_ / 100) * 255 }
$Red, $Green, $Blue = $Rgb
$Color1 = [RGBColor]::new($Red, $Green, $Blue)
$Color2 = [HSVColor]::FromRGBColor($Color1)
$Hue = $Color2.GetHue()
$AngleInDegree = $Hue.GetAngleInDegree()
$Diff = [Math]::Abs($AngleInDegree - 61.8)
$Diff | Should -BeLessThan 0.1
$Saturation = $Color2.GetSaturation()
$Diff = [Math]::Abs($Saturation.GetSaturation() - 0.779)
$Diff | Should -BeLessThan 0.01
$Value = $Color2.GetValue()
$Diff = [Math]::Abs($Value.GetValue() - 0.643)
$Diff | Should -BeLessThan 0.01
}
}
}
}
テスト結果
こんな感じになりました。
PS > .\mycolor.Tests.ps1
Describing RGBColor
Context 正常
[+] 赤を返す。 1ms
[+] 緑を返す。 1ms
[+] 青を返す。 3ms
[+] すべて0で例外を発生しない。 0ms
[+] すべて255で例外を発生しない。 0ms
Context 異常
[+] 色の要素が0未満ならば例外を発生する。 2ms
[+] 色の要素が256以上ならば例外を発生する。 1ms
Describing AngleInDegree
Context 正常
[+] 0度を返す。 1ms
[+] 359度を返す。 1ms
Context 異常
[+] 0未満ならば例外を発生する。 0ms
[+] 360度以上ならば例外を発生する。 2ms
Describing HSVSaturation
Context 正常
[+] 0を返す。 1ms
[+] 1を返す。 10ms
Context 異常
[+] 0未満ならば例外を発生する。 1ms
[+] 1を超えるならば例外を発生する。 0ms
Describing HSVValue
Context 正常
[+] 0を返す。 1ms
[+] 1を返す。 1ms
Context 異常
[+] 0未満ならば例外を発生する。 0ms
[+] 1を超えるならば例外を発生する。 0ms
Describing HSVColor
Context RGB to HSV
Context 正常
[+] 色相0度を返す。 1ms
[+] 色相60度を返す。 1ms
[+] 色相120度を返す。 1ms
[+] 色相180度を返す。 0ms
[+] 色相240度を返す。 2ms
[+] 色相300度を返す。 1ms
[+] 彩度を返す。 1ms
[+] 明度を返す。 1ms
[+] RGB(100%,100%,100%)は期待通りのHSVを返す。 3ms
[+] RGB(0%,0%,0%)は期待通りのHSVを返す。 2ms
[+] RGB(62.8%,64.3%,14.2%)は期待通りのHSVを返す。 2ms
PS >
簡単ですね!
まとめ
- RGB を独自のHSV変換で、色相(Hue)、彩度(Saturation)、明度(Value) に変換できました。
- もうちょっといろいろ遊べそうですね!
現場からは以上です。
Discussion