diff --git a/internal/models/photo.go b/internal/models/photo.go index 7a2935eb4..25cf33c45 100644 --- a/internal/models/photo.go +++ b/internal/models/photo.go @@ -18,6 +18,8 @@ type Photo struct { PhotoArtist string PhotoColors string PhotoColor string + PhotoLuminance string + PhotoMonochrome bool PhotoCanonicalName string PhotoFavorite bool PhotoLat float64 diff --git a/internal/photoprism/colors.go b/internal/photoprism/colors.go index 6ad3ffb2e..e8fd59ad4 100644 --- a/internal/photoprism/colors.go +++ b/internal/photoprism/colors.go @@ -2,8 +2,10 @@ package photoprism import ( "fmt" + "image" "image/color" "log" + "math" "github.com/disintegration/imaging" "github.com/lucasb-eyer/go-colorful" @@ -12,6 +14,9 @@ import ( type MaterialColor uint16 type MaterialColors []MaterialColor +type Luminance uint8 +type LightMap []Luminance + const ColorSampleSize = 3 const ( @@ -87,6 +92,18 @@ func (c MaterialColors) Hex() (result string) { return result } +func (l Luminance) Hex() string { + return fmt.Sprintf("%X", l) +} + +func (m LightMap) Hex() (result string) { + for _, luminance := range m { + result += luminance.Hex() + } + + return result +} + var materialColorMap = map[color.RGBA]MaterialColor{ {0x00, 0x00, 0x00, 0xff}: Black, {0x79, 0x55, 0x48, 0xff}: Brown, @@ -122,28 +139,35 @@ func colorfulToMaterialColor(actualColor colorful.Color) (result MaterialColor) return result } -// Colors returns color information for a media file. -func (m *MediaFile) Colors() (colors MaterialColors, mainColor MaterialColor, err error) { +func (m *MediaFile) Resize(width, height int) (result *image.NRGBA, err error) { jpeg, err := m.GetJpeg() if err != nil { - log.Printf("can't find jpeg: %s", err.Error()) - - return colors, mainColor, err + return nil, err } - img, err := imaging.Open(jpeg.GetFilename(), imaging.AutoOrientation(true)) + img, err:= imaging.Open(jpeg.GetFilename(), imaging.AutoOrientation(true)) if err != nil { - log.Printf("can't open jpeg: %s", err.Error()) - - return colors, mainColor, err + return nil, err } - img = imaging.Resize(img, ColorSampleSize, ColorSampleSize, imaging.Box) + return imaging.Resize(img, width, height, imaging.Box), nil +} + +// Colors returns color information for a media file. +func (m *MediaFile) Colors() (colors MaterialColors, mainColor MaterialColor, luminance LightMap, monochrome bool, err error) { + img, err := m.Resize(ColorSampleSize, ColorSampleSize) + + if err != nil { + log.Printf("can't open image: %s", err.Error()) + + return colors, mainColor, luminance, monochrome, err + } bounds := img.Bounds() width, height := bounds.Max.X, bounds.Max.Y + monochrome = true colorCount := make(map[MaterialColor]uint16) var mainColorCount uint16 @@ -166,8 +190,15 @@ func (m *MediaFile) Colors() (colors MaterialColors, mainColor MaterialColor, er mainColor = materialColor } + _, s, l := rgbColor.Hsl() + + if s != 0 { + monochrome = false + } + + luminance = append(luminance, Luminance(math.Round(l * 16))) } } - return colors, mainColor, nil + return colors, mainColor, luminance, monochrome, nil } diff --git a/internal/photoprism/colors_slow_test.go b/internal/photoprism/colors_slow_test.go index 9d701e7b0..c89586090 100644 --- a/internal/photoprism/colors_slow_test.go +++ b/internal/photoprism/colors_slow_test.go @@ -15,11 +15,12 @@ func TestMediaFile_GetColors_Slow(t *testing.T) { conf.InitializeTestData(t) if mediaFile2, err := NewMediaFile(conf.ImportPath() + "/iphone/IMG_6788.JPG"); err == nil { - colors, main, err := mediaFile2.Colors() + colors, main, l, m, err := mediaFile2.Colors() - t.Log(colors, main, err) + t.Log(colors, main, l, m, err) assert.Nil(t, err) + assert.False(t, m) assert.IsType(t, MaterialColors{}, colors) assert.Equal(t, "grey", main.Name()) assert.Equal(t, MaterialColors{0x2, 0x1, 0x2, 0x1, 0x1, 0x1, 0x2, 0x1, 0x2}, colors) @@ -28,11 +29,12 @@ func TestMediaFile_GetColors_Slow(t *testing.T) { } if mediaFile3, err := NewMediaFile(conf.ImportPath() + "/raw/20140717_154212_1EC48F8489.jpg"); err == nil { - colors, main, err := mediaFile3.Colors() + colors, main, l, m, err := mediaFile3.Colors() - t.Log(colors, main, err) + t.Log(colors, main, l, m, err) assert.Nil(t, err) + assert.False(t, m) assert.IsType(t, MaterialColors{}, colors) assert.Equal(t, "grey", main.Name()) diff --git a/internal/photoprism/colors_test.go b/internal/photoprism/colors_test.go index 7b42d5de0..79dc9887e 100644 --- a/internal/photoprism/colors_test.go +++ b/internal/photoprism/colors_test.go @@ -13,27 +13,31 @@ func TestMediaFile_GetColors(t *testing.T) { conf.InitializeTestData(t) if mediaFile1, err := NewMediaFile(conf.ImportPath() + "/dog.jpg"); err == nil { - colors, main, err := mediaFile1.Colors() + colors, main, l, m, err := mediaFile1.Colors() - t.Log(colors, main, err) + t.Log(colors, main, l, m, err) assert.Nil(t, err) + assert.False(t, m) assert.IsType(t, MaterialColors{}, colors) assert.Equal(t, "grey", main.Name()) assert.Equal(t, MaterialColors{0x1, 0x2, 0x1, 0x2, 0x2, 0x1, 0x1, 0x1, 0x0}, colors) + assert.Equal(t, LightMap{5, 9, 7, 10, 9, 5, 5, 6, 2}, l) } else { t.Error(err) } if mediaFile2, err := NewMediaFile(conf.ImportPath() + "/ape.jpeg"); err == nil { - colors, main, err := mediaFile2.Colors() + colors, main, l, m, err := mediaFile2.Colors() - t.Log(colors, main, err) + t.Log(colors, main, l, m, err) assert.Nil(t, err) + assert.False(t, m) assert.IsType(t, MaterialColors{}, colors) assert.Equal(t, "teal", main.Name()) assert.Equal(t, MaterialColors{0x8, 0x8, 0x2, 0x8, 0x2, 0x1, 0x8, 0x1, 0x2}, colors) + assert.Equal(t, LightMap{8, 8, 7, 7, 7, 5, 8, 6, 8}, l) } else { t.Error(err) } diff --git a/internal/photoprism/indexer.go b/internal/photoprism/indexer.go index 4542c4e69..9d9cf1aa6 100644 --- a/internal/photoprism/indexer.go +++ b/internal/photoprism/indexer.go @@ -94,10 +94,12 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string { } // PhotoColors - photoColors, photoColor, _ := jpeg.Colors() + photoColors, photoColor, luminance, monochrome, _ := jpeg.Colors() photo.PhotoColor = photoColor.Name() photo.PhotoColors = photoColors.Hex() + photo.PhotoLuminance = luminance.Hex() + photo.PhotoMonochrome = monochrome // Tags (TensorFlow) tags = i.getImageTags(jpeg) @@ -163,10 +165,12 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string { } else if time.Now().Sub(photo.UpdatedAt).Minutes() > 10 { // If updated more than 10 minutes ago if jpeg, err := mediaFile.GetJpeg(); err == nil { // PhotoColors - photoColors, photoColor, _ := jpeg.Colors() + photoColors, photoColor, luminance, monochrome, _ := jpeg.Colors() photo.PhotoColor = photoColor.Name() photo.PhotoColors = photoColors.Hex() + photo.PhotoLuminance = luminance.Hex() + photo.PhotoMonochrome = monochrome photo.Camera = models.NewCamera(mediaFile.GetCameraModel(), mediaFile.GetCameraMake()).FirstOrCreate(i.db) photo.Lens = models.NewLens(mediaFile.GetLensModel(), mediaFile.GetLensMake()).FirstOrCreate(i.db)