func DefaultOptionsFor(m *image.YCbCr) *Options { r := m.Bounds() return &Options{ Whiteness: 100, LineWidth: r.Dx() / 20, Desaturate: true, } }
func process(m *image.YCbCr) { dx := float64(m.Bounds().Dx()) opts := &cleanup.Options{ Whiteness: *white, LineWidth: int(*lineWidth * dx), Desaturate: !*colored, } cleanup.ByBase(m, opts) }
// encode image.YCbCr func encodeYCbCr(cinfo *C.struct_jpeg_compress_struct, src *image.YCbCr, p *EncoderOptions) (err error) { // Set up compression parameters cinfo.image_width = C.JDIMENSION(src.Bounds().Dx()) cinfo.image_height = C.JDIMENSION(src.Bounds().Dy()) cinfo.input_components = 3 cinfo.in_color_space = C.JCS_YCbCr C.jpeg_set_defaults(cinfo) setupEncoderOptions(cinfo, p) compInfo := (*[3]C.jpeg_component_info)(unsafe.Pointer(cinfo.comp_info)) colorVDiv := 1 switch src.SubsampleRatio { case image.YCbCrSubsampleRatio444: // 1x1,1x1,1x1 compInfo[Y].h_samp_factor, compInfo[Y].v_samp_factor = 1, 1 compInfo[Cb].h_samp_factor, compInfo[Cb].v_samp_factor = 1, 1 compInfo[Cr].h_samp_factor, compInfo[Cr].v_samp_factor = 1, 1 case image.YCbCrSubsampleRatio440: // 1x2,1x1,1x1 compInfo[Y].h_samp_factor, compInfo[Y].v_samp_factor = 1, 2 compInfo[Cb].h_samp_factor, compInfo[Cb].v_samp_factor = 1, 1 compInfo[Cr].h_samp_factor, compInfo[Cr].v_samp_factor = 1, 1 colorVDiv = 2 case image.YCbCrSubsampleRatio422: // 2x1,1x1,1x1 compInfo[Y].h_samp_factor, compInfo[Y].v_samp_factor = 2, 1 compInfo[Cb].h_samp_factor, compInfo[Cb].v_samp_factor = 1, 1 compInfo[Cr].h_samp_factor, compInfo[Cr].v_samp_factor = 1, 1 case image.YCbCrSubsampleRatio420: // 2x2,1x1,1x1 compInfo[Y].h_samp_factor, compInfo[Y].v_samp_factor = 2, 2 compInfo[Cb].h_samp_factor, compInfo[Cb].v_samp_factor = 1, 1 compInfo[Cr].h_samp_factor, compInfo[Cr].v_samp_factor = 1, 1 colorVDiv = 2 } // libjpeg raw data in is in planar format, which avoids unnecessary // planar->packed->planar conversions. cinfo.raw_data_in = C.TRUE // Start compression C.jpeg_start_compress(cinfo, C.TRUE) C.encode_ycbcr( cinfo, C.JSAMPROW(unsafe.Pointer(&src.Y[0])), C.JSAMPROW(unsafe.Pointer(&src.Cb[0])), C.JSAMPROW(unsafe.Pointer(&src.Cr[0])), C.int(src.YStride), C.int(src.CStride), C.int(colorVDiv), ) C.jpeg_finish_compress(cinfo) return }
// encodePGM encodes gotImage in the PGM format in the IMC4 layout. func encodePGM(gotImage image.Image) ([]byte, error) { var ( m *image.YCbCr ma *nycbcra.Image ) switch g := gotImage.(type) { case *image.YCbCr: m = g case *nycbcra.Image: m = &g.YCbCr ma = g default: return nil, fmt.Errorf("lossy image did not decode to an *image.YCbCr") } if m.SubsampleRatio != image.YCbCrSubsampleRatio420 { return nil, fmt.Errorf("lossy image did not decode to a 4:2:0 YCbCr") } b := m.Bounds() w, h := b.Dx(), b.Dy() w2, h2 := (w+1)/2, (h+1)/2 outW, outH := 2*w2, h+h2 if ma != nil { outH += h } buf := new(bytes.Buffer) fmt.Fprintf(buf, "P5\n%d %d\n255\n", outW, outH) for y := b.Min.Y; y < b.Max.Y; y++ { o := m.YOffset(b.Min.X, y) buf.Write(m.Y[o : o+w]) if w&1 != 0 { buf.WriteByte(0x00) } } for y := b.Min.Y; y < b.Max.Y; y += 2 { o := m.COffset(b.Min.X, y) buf.Write(m.Cb[o : o+w2]) buf.Write(m.Cr[o : o+w2]) } if ma != nil { for y := b.Min.Y; y < b.Max.Y; y++ { o := ma.AOffset(b.Min.X, y) buf.Write(ma.A[o : o+w]) if w&1 != 0 { buf.WriteByte(0x00) } } } return buf.Bytes(), nil }
// ByBase cleans image based on the surrounding color values func ByBase(m *image.YCbCr, opts *Options) { if opts == nil { opts = DefaultOptionsFor(m) } white := opts.Whiteness * 255.0 / 100.0 L := &filter.Channel{ Data: m.Y, Width: m.Bounds().Dx(), Height: m.Bounds().Dy(), Stride: m.YStride, } // get rid of hot-pixels L.Median(1) if opts.Desaturate { filter.Desaturate(m) } base := L.Clone() base.Erode(opts.LineWidth) base.Blur(opts.LineWidth) average := base.Average() invspan := 1.0 / (average / white) for y := 0; y < L.Height; y++ { i := y * L.Stride e := i + L.Width for ; i < e; i++ { lv := float64(L.Data[i]) bv := float64(base.Data[i]) r := int(white + (lv-bv)*invspan) if r < 0x00 { r = 0x00 } else if r > 0xFF { r = 0xFF } L.Data[i] = byte(r) } } }
func testDecodeLossy(t *testing.T, tc string, withAlpha bool) { webpFilename := "../testdata/" + tc + ".lossy.webp" pngFilename := webpFilename + ".ycbcr.png" if withAlpha { webpFilename = "../testdata/" + tc + ".lossy-with-alpha.webp" pngFilename = webpFilename + ".nycbcra.png" } f0, err := os.Open(webpFilename) if err != nil { t.Errorf("%s: Open WEBP: %v", tc, err) return } defer f0.Close() img0, err := Decode(f0) if err != nil { t.Errorf("%s: Decode WEBP: %v", tc, err) return } var ( m0 *image.YCbCr a0 *image.NYCbCrA ok bool ) if withAlpha { a0, ok = img0.(*image.NYCbCrA) if ok { m0 = &a0.YCbCr } } else { m0, ok = img0.(*image.YCbCr) } if !ok || m0.SubsampleRatio != image.YCbCrSubsampleRatio420 { t.Errorf("%s: decoded WEBP image is not a 4:2:0 YCbCr or 4:2:0 NYCbCrA", tc) return } // w2 and h2 are the half-width and half-height, rounded up. w, h := m0.Bounds().Dx(), m0.Bounds().Dy() w2, h2 := int((w+1)/2), int((h+1)/2) f1, err := os.Open(pngFilename) if err != nil { t.Errorf("%s: Open PNG: %v", tc, err) return } defer f1.Close() img1, err := png.Decode(f1) if err != nil { t.Errorf("%s: Open PNG: %v", tc, err) return } // The split-into-YCbCr-planes golden image is a 2*w2 wide and h+h2 high // (or 2*h+h2 high, if with Alpha) gray image arranged in IMC4 format: // YYYY // YYYY // BBRR // AAAA // See http://www.fourcc.org/yuv.php#IMC4 pngW, pngH := 2*w2, h+h2 if withAlpha { pngH += h } if got, want := img1.Bounds(), image.Rect(0, 0, pngW, pngH); got != want { t.Errorf("%s: bounds0: got %v, want %v", tc, got, want) return } m1, ok := img1.(*image.Gray) if !ok { t.Errorf("%s: decoded PNG image is not a Gray", tc) return } type plane struct { name string m0Pix []uint8 m0Stride int m1Rect image.Rectangle } planes := []plane{ {"Y", m0.Y, m0.YStride, image.Rect(0, 0, w, h)}, {"Cb", m0.Cb, m0.CStride, image.Rect(0*w2, h, 1*w2, h+h2)}, {"Cr", m0.Cr, m0.CStride, image.Rect(1*w2, h, 2*w2, h+h2)}, } if withAlpha { planes = append(planes, plane{ "A", a0.A, a0.AStride, image.Rect(0, h+h2, w, 2*h+h2), }) } for _, plane := range planes { dx := plane.m1Rect.Dx() nDiff, diff := 0, make([]byte, dx) for j, y := 0, plane.m1Rect.Min.Y; y < plane.m1Rect.Max.Y; j, y = j+1, y+1 { got := plane.m0Pix[j*plane.m0Stride:][:dx] want := m1.Pix[y*m1.Stride+plane.m1Rect.Min.X:][:dx] if bytes.Equal(got, want) { continue } nDiff++ if nDiff > 10 { t.Errorf("%s: %s plane: more rows differ", tc, plane.name) break } for i := range got { diff[i] = got[i] - want[i] } t.Errorf("%s: %s plane: m0 row %d, m1 row %d\ngot %s\nwant%s\ndiff%s", tc, plane.name, j, y, hex(got), hex(want), hex(diff)) } } }
// resizeYCbCr returns a scaled copy of the YCbCr image slice r of m. // The returned image has width w and height h. func resizeYCbCr(m *image.YCbCr, r image.Rectangle, w, h int) (image.Image, bool) { dst := image.NewRGBA(image.Rect(0, 0, w, h)) xdraw.ApproxBiLinear.Scale(dst, dst.Bounds(), m, m.Bounds(), xdraw.Src, nil) return dst, true }
// encode image.YCbCr func encodeYCbCr(cinfo *C.struct_jpeg_compress_struct, src *image.YCbCr, p *EncoderOptions) (err error) { // Set up compression parameters cinfo.image_width = C.JDIMENSION(src.Bounds().Dx()) cinfo.image_height = C.JDIMENSION(src.Bounds().Dy()) cinfo.input_components = 3 cinfo.in_color_space = C.JCS_YCbCr C.jpeg_set_defaults(cinfo) setupEncoderOptions(cinfo, p) compInfo := (*[3]C.jpeg_component_info)(unsafe.Pointer(cinfo.comp_info)) colorVDiv := 1 switch src.SubsampleRatio { case image.YCbCrSubsampleRatio444: // 1x1,1x1,1x1 compInfo[Y].h_samp_factor, compInfo[Y].v_samp_factor = 1, 1 compInfo[Cb].h_samp_factor, compInfo[Cb].v_samp_factor = 1, 1 compInfo[Cr].h_samp_factor, compInfo[Cr].v_samp_factor = 1, 1 case image.YCbCrSubsampleRatio440: // 1x2,1x1,1x1 compInfo[Y].h_samp_factor, compInfo[Y].v_samp_factor = 1, 2 compInfo[Cb].h_samp_factor, compInfo[Cb].v_samp_factor = 1, 1 compInfo[Cr].h_samp_factor, compInfo[Cr].v_samp_factor = 1, 1 colorVDiv = 2 case image.YCbCrSubsampleRatio422: // 2x1,1x1,1x1 compInfo[Y].h_samp_factor, compInfo[Y].v_samp_factor = 2, 1 compInfo[Cb].h_samp_factor, compInfo[Cb].v_samp_factor = 1, 1 compInfo[Cr].h_samp_factor, compInfo[Cr].v_samp_factor = 1, 1 case image.YCbCrSubsampleRatio420: // 2x2,1x1,1x1 compInfo[Y].h_samp_factor, compInfo[Y].v_samp_factor = 2, 2 compInfo[Cb].h_samp_factor, compInfo[Cb].v_samp_factor = 1, 1 compInfo[Cr].h_samp_factor, compInfo[Cr].v_samp_factor = 1, 1 colorVDiv = 2 } // libjpeg raw data in is in planar format, which avoids unnecessary // planar->packed->planar conversions. cinfo.raw_data_in = C.TRUE // Start compression C.jpeg_start_compress(cinfo, C.TRUE) // Allocate JSAMPIMAGE to hold pointers to one iMCU worth of image data // this is a safe overestimate; we use the return value from // jpeg_read_raw_data to figure out what is the actual iMCU row count. var yRowPtr [AlignSize]C.JSAMPROW var cbRowPtr [AlignSize]C.JSAMPROW var crRowPtr [AlignSize]C.JSAMPROW yCbCrPtr := [3]C.JSAMPARRAY{ C.JSAMPARRAY(unsafe.Pointer(&yRowPtr[0])), C.JSAMPARRAY(unsafe.Pointer(&cbRowPtr[0])), C.JSAMPARRAY(unsafe.Pointer(&crRowPtr[0])), } var rows C.JDIMENSION for rows = 0; rows < cinfo.image_height; { // First fill in the pointers into the plane data buffers for j := 0; j < int(C.DCTSIZE*compInfo[Y].v_samp_factor); j++ { yRowPtr[j] = C.JSAMPROW(unsafe.Pointer(&src.Y[src.YStride*(int(rows)+j)])) } for j := 0; j < int(C.DCTSIZE*compInfo[Cb].v_samp_factor); j++ { cbRowPtr[j] = C.JSAMPROW(unsafe.Pointer(&src.Cb[src.CStride*(int(rows)/colorVDiv+j)])) crRowPtr[j] = C.JSAMPROW(unsafe.Pointer(&src.Cr[src.CStride*(int(rows)/colorVDiv+j)])) } // Get the data rows += C.jpeg_write_raw_data(cinfo, C.JSAMPIMAGE(unsafe.Pointer(&yCbCrPtr[0])), C.JDIMENSION(C.DCTSIZE*compInfo[0].v_samp_factor)) } C.jpeg_finish_compress(cinfo) return }