organize a bit
This commit is contained in:
parent
95729c7228
commit
e3ddb3b8dd
26
server/bestcrop.go
Normal file
26
server/bestcrop.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"github.com/muesli/smartcrop"
|
||||||
|
"github.com/muesli/smartcrop/nfnt"
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBestPieceOfImage(width, height int, img image.Image) image.Image {
|
||||||
|
analyzer := smartcrop.NewAnalyzer(nfnt.NewDefaultResizer())
|
||||||
|
bestCrop, _ := analyzer.FindBestCrop(img, 800, 480)
|
||||||
|
|
||||||
|
fmt.Printf("Best crop: %+v\n", bestCrop)
|
||||||
|
|
||||||
|
type SubImager interface {
|
||||||
|
SubImage(r image.Rectangle) image.Image
|
||||||
|
}
|
||||||
|
croppedImg := img.(SubImager).SubImage(bestCrop)
|
||||||
|
|
||||||
|
resizedImg := resize.Resize(800, 480, croppedImg, resize.Lanczos3)
|
||||||
|
|
||||||
|
return resizedImg
|
||||||
|
}
|
81
server/einkimage.go
Normal file
81
server/einkimage.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/mattn/go-ciede2000"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Maps RGB colors to the eink display's color values
|
||||||
|
type ColorSpace []struct {
|
||||||
|
Color Colorf
|
||||||
|
Code uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func ditherPixel(image [][]Colorf, x int, y int, diff Colorf) {
|
||||||
|
height := len(image)
|
||||||
|
width := len(image[0])
|
||||||
|
|
||||||
|
if x < 0 || y < 0 || x >= width || y >= height {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
image[y][x] = clampColor(addColorf(image[y][x], diff), 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ditherFloydSteinberg(image [][]Colorf, x int, y int, color Colorf) {
|
||||||
|
ditherPixel(image, x+1, y, multColor(color, 7.0/16.0))
|
||||||
|
ditherPixel(image, x-1, y+1, multColor(color, 3.0/16.0))
|
||||||
|
ditherPixel(image, x, y+1, multColor(color, 5.0/16.0))
|
||||||
|
ditherPixel(image, x+1, y+1, multColor(color, 1.0/16.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertToEInkImage(src image.Image, colorSpace ColorSpace) []byte {
|
||||||
|
bounds := src.Bounds()
|
||||||
|
width, height := bounds.Max.X, bounds.Max.Y
|
||||||
|
|
||||||
|
image := convertImageToColors(src)
|
||||||
|
|
||||||
|
var einkImage []byte
|
||||||
|
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
// find closest color
|
||||||
|
minDist := math.MaxFloat64
|
||||||
|
bestColorCode := uint8(0x0)
|
||||||
|
bestColor := Colorf{0, 0, 0}
|
||||||
|
for _, def := range colorSpace {
|
||||||
|
dist := ciede2000.Diff(convertToRGBA(image[y][x]), convertToRGBA(def.Color))
|
||||||
|
if dist < minDist {
|
||||||
|
minDist = dist
|
||||||
|
bestColorCode = def.Code
|
||||||
|
bestColor = def.Color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
einkImage = append(einkImage, byte(bestColorCode))
|
||||||
|
|
||||||
|
// dither the remaining color using Floyd-Steinberg to create the illusion of shading
|
||||||
|
diff := subtractColorf(image[y][x], bestColor)
|
||||||
|
ditherFloydSteinberg(image, x, y, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// each byte holds two pixels for eink display
|
||||||
|
einkImage = packBytesIntoNibbles(einkImage)
|
||||||
|
|
||||||
|
return einkImage
|
||||||
|
}
|
||||||
|
|
||||||
|
func packBytesIntoNibbles(input []byte) []byte {
|
||||||
|
// input length must divisible by 2
|
||||||
|
|
||||||
|
var output []byte
|
||||||
|
|
||||||
|
for x := 0; x < len(input); x += 2 {
|
||||||
|
output = append(output, (input[x]<<4)|input[x+1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
243
server/main.go
243
server/main.go
@ -3,25 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/draw"
|
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
|
||||||
"github.com/mattn/go-ciede2000"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"github.com/muesli/smartcrop"
|
|
||||||
"github.com/muesli/smartcrop/nfnt"
|
|
||||||
"github.com/nfnt/resize"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Define a function to handle incoming requests
|
// Define a function to handle incoming requests
|
||||||
@ -38,228 +27,26 @@ func requestHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
fmt.Fprintf(w, "Hello, %s!\n", name)
|
fmt.Fprintf(w, "Hello, %s!\n", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
type EInkColor uint8
|
var colorSpace = ColorSpace{
|
||||||
|
{Colorf{0, 0, 0}, 0x0},
|
||||||
const (
|
{Colorf{1, 1, 1}, 0x1},
|
||||||
EPD_7IN3F_BLACK EInkColor = 0x0
|
{Colorf{0.059, 0.329, 0.119}, 0x2},
|
||||||
EPD_7IN3F_WHITE EInkColor = 0x1
|
{Colorf{0.061, 0.147, 0.336}, 0x3},
|
||||||
EPD_7IN3F_GREEN EInkColor = 0x2
|
{Colorf{0.574, 0.066, 0.010}, 0x4},
|
||||||
EPD_7IN3F_BLUE EInkColor = 0x3
|
{Colorf{0.982, 0.756, 0.004}, 0x5},
|
||||||
EPD_7IN3F_RED EInkColor = 0x4
|
{Colorf{0.795, 0.255, 0.018}, 0x6},
|
||||||
EPD_7IN3F_YELLOW EInkColor = 0x5
|
|
||||||
EPD_7IN3F_ORANGE EInkColor = 0x6
|
|
||||||
)
|
|
||||||
|
|
||||||
type ColorDefinition struct {
|
|
||||||
Color Colorf
|
|
||||||
Code EInkColor
|
|
||||||
}
|
|
||||||
|
|
||||||
var Colors = []ColorDefinition{
|
|
||||||
{Colorf{0, 0, 0}, EPD_7IN3F_BLACK},
|
|
||||||
{Colorf{1, 1, 1}, EPD_7IN3F_WHITE},
|
|
||||||
{Colorf{0.059, 0.329, 0.119}, EPD_7IN3F_GREEN},
|
|
||||||
{Colorf{0.061, 0.147, 0.336}, EPD_7IN3F_BLUE},
|
|
||||||
{Colorf{0.574, 0.066, 0.010}, EPD_7IN3F_RED},
|
|
||||||
{Colorf{0.982, 0.756, 0.004}, EPD_7IN3F_YELLOW},
|
|
||||||
{Colorf{0.795, 0.255, 0.018}, EPD_7IN3F_ORANGE},
|
|
||||||
}
|
|
||||||
|
|
||||||
func imageToRGBA(src image.Image) *image.RGBA {
|
|
||||||
// No conversion needed if image is an *image.RGBA.
|
|
||||||
if dst, ok := src.(*image.RGBA); ok {
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the image/draw package to convert to *image.RGBA.
|
|
||||||
b := src.Bounds()
|
|
||||||
dst := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
|
||||||
draw.Draw(dst, dst.Bounds(), src, b.Min, draw.Src)
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// Colorf represents a color with float64 RGB values.
|
|
||||||
type Colorf struct {
|
|
||||||
R, G, B float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert an image to a 2D array of Colorf.
|
|
||||||
func convertImageToColors(src image.Image) [][]Colorf {
|
|
||||||
img := imageToRGBA(src)
|
|
||||||
|
|
||||||
bounds := img.Bounds()
|
|
||||||
width, height := bounds.Max.X, bounds.Max.Y
|
|
||||||
|
|
||||||
// Initialize the 2D slice.
|
|
||||||
colorfImg := make([][]Colorf, height)
|
|
||||||
for i := range colorfImg {
|
|
||||||
colorfImg[i] = make([]Colorf, width)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over each pixel.
|
|
||||||
for y := 0; y < height; y++ {
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
r, g, b, _ := img.At(x, y).RGBA()
|
|
||||||
|
|
||||||
// Convert the uint32 color values to float64, normalized to [0, 1].
|
|
||||||
colorf := Colorf{
|
|
||||||
R: float64(r) / 0xffff,
|
|
||||||
G: float64(g) / 0xffff,
|
|
||||||
B: float64(b) / 0xffff,
|
|
||||||
}
|
|
||||||
|
|
||||||
colorfImg[y][x] = colorf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return colorfImg
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertToRGBA(colorf Colorf) color.RGBA {
|
|
||||||
// Convert the float64 color values to uint8, and ignore the alpha channel.
|
|
||||||
return color.RGBA{
|
|
||||||
R: uint8(colorf.R * 255),
|
|
||||||
G: uint8(colorf.G * 255),
|
|
||||||
B: uint8(colorf.B * 255),
|
|
||||||
A: 255,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func subtractColorf(c1, c2 Colorf) Colorf {
|
|
||||||
return Colorf{
|
|
||||||
R: c1.R - c2.R,
|
|
||||||
G: c1.G - c2.G,
|
|
||||||
B: c1.B - c2.B,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addColorf(c1, c2 Colorf) Colorf {
|
|
||||||
return Colorf{
|
|
||||||
R: c1.R + c2.R,
|
|
||||||
G: c1.G + c2.G,
|
|
||||||
B: c1.B + c2.B,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func multColor(c Colorf, m float64) Colorf {
|
|
||||||
return Colorf{
|
|
||||||
R: c.R * m,
|
|
||||||
G: c.G * m,
|
|
||||||
B: c.B * m,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func clampColor(value Colorf, min, max float64) Colorf {
|
|
||||||
return Colorf{
|
|
||||||
R: clamp(value.R, min, max),
|
|
||||||
G: clamp(value.G, min, max),
|
|
||||||
B: clamp(value.B, min, max),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func clamp(value, min, max float64) float64 {
|
|
||||||
if value < min {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
if value > max {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func ditherFloydSteinberg(image [][]Colorf, x int, y int, diff Colorf) {
|
|
||||||
height := len(image)
|
|
||||||
width := len(image[0])
|
|
||||||
|
|
||||||
if x < 0 || y < 0 || x >= width || y >= height {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
image[y][x] = clampColor(addColorf(image[y][x], diff), 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConvertToEInkImage(src image.Image) []byte {
|
|
||||||
bounds := src.Bounds()
|
|
||||||
width, height := bounds.Max.X, bounds.Max.Y
|
|
||||||
|
|
||||||
image := convertImageToColors(src)
|
|
||||||
|
|
||||||
var einkImage []byte
|
|
||||||
|
|
||||||
for y := 0; y < height; y++ {
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
// find closest color
|
|
||||||
minDist := math.MaxFloat64
|
|
||||||
bestColorCode := EInkColor(0)
|
|
||||||
bestColor := Colorf{0, 0, 0}
|
|
||||||
for _, def := range Colors {
|
|
||||||
dist := ciede2000.Diff(convertToRGBA(image[y][x]), convertToRGBA(def.Color))
|
|
||||||
if dist < minDist {
|
|
||||||
minDist = dist
|
|
||||||
bestColorCode = def.Code
|
|
||||||
bestColor = def.Color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
einkImage = append(einkImage, byte(bestColorCode))
|
|
||||||
|
|
||||||
// dither colors using Floyd-Steinberg to create the illusion of shading
|
|
||||||
diff := subtractColorf(image[y][x], bestColor)
|
|
||||||
|
|
||||||
ditherFloydSteinberg(image, x+1, y, multColor(diff, 7.0/16.0))
|
|
||||||
ditherFloydSteinberg(image, x-1, y+1, multColor(diff, 3.0/16.0))
|
|
||||||
ditherFloydSteinberg(image, x, y+1, multColor(diff, 5.0/16.0))
|
|
||||||
ditherFloydSteinberg(image, x+1, y+1, multColor(diff, 1.0/16.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
einkImage = packBytesIntoNibbles(einkImage)
|
|
||||||
|
|
||||||
return einkImage
|
|
||||||
}
|
|
||||||
|
|
||||||
func packBytesIntoNibbles(input []byte) []byte {
|
|
||||||
// input must divisible by 2
|
|
||||||
|
|
||||||
var output []byte
|
|
||||||
|
|
||||||
for x := 0; x < len(input); x += 2 {
|
|
||||||
output = append(output, (input[x]<<4)|input[x+1])
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getImage(w http.ResponseWriter, r *http.Request) {
|
func getImage(w http.ResponseWriter, r *http.Request) {
|
||||||
// Open the file
|
// img := ReadImage("image.jpg")
|
||||||
// file, err := os.Open("image.jpg")
|
// img := ReadImage("image-out.jpg")
|
||||||
// file, err := os.Open("image-out.jpg")
|
img := ReadImage("dahlia-out.jpg")
|
||||||
file, err := os.Open("dahlia-out.jpg")
|
bestCrop := GetBestPieceOfImage(800, 480, img)
|
||||||
if err != nil {
|
data := ConvertToEInkImage(bestCrop, colorSpace)
|
||||||
fmt.Println("Error: File could not be opened")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
img, _, _ := image.Decode(file)
|
|
||||||
|
|
||||||
analyzer := smartcrop.NewAnalyzer(nfnt.NewDefaultResizer())
|
|
||||||
topCrop, _ := analyzer.FindBestCrop(img, 800, 480)
|
|
||||||
|
|
||||||
fmt.Printf("Top crop: %+v\n", topCrop)
|
|
||||||
|
|
||||||
type SubImager interface {
|
|
||||||
SubImage(r image.Rectangle) image.Image
|
|
||||||
}
|
|
||||||
croppedImg := img.(SubImager).SubImage(topCrop)
|
|
||||||
|
|
||||||
resizedImg := resize.Resize(800, 480, croppedImg, resize.Lanczos3)
|
|
||||||
|
|
||||||
data := ConvertToEInkImage(resizedImg)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
|
||||||
fmt.Printf("Bytes to send: %+v\n", len(data))
|
fmt.Printf("Bytes to send: %+v\n", len(data))
|
||||||
|
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,7 +62,7 @@ func basicAuth(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("Hello, Nix!")
|
fmt.Println("Starting server")
|
||||||
|
|
||||||
// Create a new Chi router
|
// Create a new Chi router
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user