From 4a1ba9d5fcafc6aae6977ca6226996933b9d8677 Mon Sep 17 00:00:00 2001 From: Peri Telemachou Date: Thu, 28 Sep 2023 18:11:16 +1000 Subject: [PATCH] Add GDALNearblack binding (#114) --- errors.go | 4 + godal.cpp | 21 ++++ godal.go | 78 ++++++++++++- godal.h | 1 + godal_test.go | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++ options.go | 9 ++ 6 files changed, 413 insertions(+), 2 deletions(-) diff --git a/errors.go b/errors.go index faf4540..8c7f939 100644 --- a/errors.go +++ b/errors.go @@ -151,6 +151,7 @@ func ErrLogger(fn ErrorHandler) interface { SetStatisticsOption ClearStatisticsOption GridOption + NearblackOption } { return errorCallback{fn} } @@ -368,6 +369,9 @@ func (ec errorCallback) setGridCreateOpt(o *gridCreateOpts) { func (ec errorCallback) setGridOpt(o *gridOpts) { o.errorHandler = ec.fn } +func (ec errorCallback) setNearblackOpt(o *nearBlackOpts) { + o.errorHandler = ec.fn +} type multiError struct { errs []error diff --git a/godal.cpp b/godal.cpp index 4836228..5218366 100644 --- a/godal.cpp +++ b/godal.cpp @@ -1744,6 +1744,27 @@ GDALDatasetH godalGrid(cctx *ctx, const char *pszDest, GDALDatasetH hSrcDS, char forceError(ctx); } + godalUnwrap(); + return ret; +} + +GDALDatasetH godalNearblack(cctx *ctx, const char *pszDest, GDALDatasetH hDstDS, GDALDatasetH hSrcDS, char **switches) { + godalWrap(ctx); + + GDALNearblackOptions *nbopts = GDALNearblackOptionsNew(switches,nullptr); + if(failed(ctx)) { + GDALNearblackOptionsFree(nbopts); + godalUnwrap(); + return nullptr; + } + + int usageErr=0; + GDALDatasetH ret = GDALNearblack(pszDest, hDstDS, hSrcDS, nbopts, &usageErr); + GDALNearblackOptionsFree(nbopts); + if(ret==nullptr || usageErr!=0) { + forceError(ctx); + } + godalUnwrap(); return ret; } \ No newline at end of file diff --git a/godal.go b/godal.go index 41a16ca..74c8614 100644 --- a/godal.go +++ b/godal.go @@ -210,7 +210,7 @@ func (band Band) ClearNoData(opts ...SetNoDataOption) error { return cgc.close() } -//SetScaleOffset sets the band's scale and offset +// SetScaleOffset sets the band's scale and offset func (band Band) SetScaleOffset(scale, offset float64, opts ...SetScaleOffsetOption) error { setterOpts := &setScaleOffsetOpts{} for _, opt := range opts { @@ -891,7 +891,7 @@ func (ds *Dataset) SetNoData(nd float64, opts ...SetNoDataOption) error { return cgc.close() } -//SetScale sets the band's scale and offset +// SetScaleOffset sets the band's scale and offset func (ds *Dataset) SetScaleOffset(scale, offset float64, opts ...SetScaleOffsetOption) error { setterOpts := &setScaleOffsetOpts{} for _, opt := range opts { @@ -3849,6 +3849,80 @@ func (ds *Dataset) Grid(destPath string, switches []string, opts ...GridOption) return &Dataset{majorObject{C.GDALMajorObjectH(dsRet)}}, nil } +// Nearblack runs the library version of nearblack +// +// See the nearblack doc page to determine the valid flags/opts that can be set in switches. +// +// Example switches : +// +// []string{"-white", "-near", "10"} +// +// Creation options and driver may be set in the switches slice with +// +// switches:=[]string{"-co","TILED=YES","-of","GTiff"} +// +// NOTE: Some switches are NOT compatible with this binding, as a `nullptr` is passed to a later call to +// `GDALNearblackOptionsNew()` (as the 2nd argument). Those switches are: "-o", "-q", "-quiet" +func (ds *Dataset) Nearblack(dstDS string, switches []string, opts ...NearblackOption) (*Dataset, error) { + nearBlackOpts := nearBlackOpts{} + for _, opt := range opts { + opt.setNearblackOpt(&nearBlackOpts) + } + + cswitches := sliceToCStringArray(switches) + defer cswitches.free() + + dest := unsafe.Pointer(C.CString(dstDS)) + defer C.free(dest) + + cgc := createCGOContext(nil, nearBlackOpts.errorHandler) + + ret, err := C.godalNearblack(cgc.cPointer(), (*C.char)(dest), nil, ds.handle(), cswitches.cPointer()) + if err = cgc.close(); err != nil { + return nil, err + } + + return &Dataset{majorObject{C.GDALMajorObjectH(ret)}}, nil +} + +// NearblackInto writes the provided `sourceDs` into the Dataset that this method was called on, and +// runs the library version of nearblack. +// +// See the nearblack doc page to determine the valid flags/opts that can be set in switches. +// +// Example switches : +// +// []string{"-white", "-near", "10"} +// +// Creation options and driver may be set in the switches slice with +// +// switches:=[]string{"-co","TILED=YES","-of","GTiff"} +// +// NOTE: Some switches are NOT compatible with this binding, as a `nullptr` is passed to a later call to +// `GDALNearblackOptionsNew()` (as the 2nd argument). Those switches are: "-o", "-q", "-quiet" +func (ds *Dataset) NearblackInto(sourceDs *Dataset, switches []string, opts ...NearblackOption) error { + nearBlackOpts := nearBlackOpts{} + for _, opt := range opts { + opt.setNearblackOpt(&nearBlackOpts) + } + + cswitches := sliceToCStringArray(switches) + defer cswitches.free() + + cgc := createCGOContext(nil, nearBlackOpts.errorHandler) + + var srcDsHandle C.GDALDatasetH = nil + if sourceDs != nil { + srcDsHandle = sourceDs.handle() + } + _ = C.godalNearblack(cgc.cPointer(), nil, ds.handle(), srcDsHandle, cswitches.cPointer()) + if err := cgc.close(); err != nil { + return err + } + + return nil +} + type cgoContext struct { cctx *C.cctx opts cStringArray diff --git a/godal.h b/godal.h index f394625..b5e7c09 100644 --- a/godal.h +++ b/godal.h @@ -153,6 +153,7 @@ extern "C" { void godalSetRasterStatistics(cctx *ctx, GDALRasterBandH bnd, double dfMin, double dfMax, double dfMean, double dfStdDev); void godalGridCreate(cctx *ctx, char *pszAlgorithm, GDALGridAlgorithm eAlgorithm, GUInt32 nPoints, const double *padfX, const double *padfY, const double *padfZ, double dfXMin, double dfXMax, double dfYMin, double dfYMax, GUInt32 nXSize, GUInt32 nYSize, GDALDataType eType, void *pData); GDALDatasetH godalGrid(cctx *ctx, const char *pszDest, GDALDatasetH hSrcDS, char **switches); + GDALDatasetH godalNearblack(cctx *ctx, const char *pszDest, GDALDatasetH hDstDS, GDALDatasetH hSrcDS, char **switches); #ifdef __cplusplus } #endif diff --git a/godal_test.go b/godal_test.go index 61d2525..e9f8bfd 100644 --- a/godal_test.go +++ b/godal_test.go @@ -4259,3 +4259,305 @@ func TestGridInvalidSwitch(t *testing.T) { _, err = vrtDs.Grid("/vsimem/test.tiff", []string{"-invalidswitch"}, ErrLogger(ehc.ErrorHandler)) assert.Error(t, err) } + +func TestNearblackBlack(t *testing.T) { + // 1. Create an image, linearly interpolated, from black (on the left) to white (on the right), using `Grid()` + var ( + outXSize = 256 + outYSize = 256 + ) + vrtDs, err := CreateVector(Memory, "") + if err != nil { + t.Error(err) + return + } + defer vrtDs.Close() + geom, err := NewGeometryFromWKT("POLYGON((0 0 0, 0 1 0, 1 1 255, 1 0 255))", nil) + if err != nil { + t.Error(err) + return + } + defer geom.Close() + _, err = vrtDs.CreateLayer("grid", nil, GTPolygon) + if err != nil { + t.Error(err) + return + } + _, err = vrtDs.Layers()[0].NewFeature(geom) + if err != nil { + t.Error(err) + return + } + // As of GDAL v3.6, `GDALGrid` will swap `yMin` and `yMax` if `yMin` < `yMax`. In order to make the output of + // earlier GDAL versions (< 3.6) consistent with this, we're setting `yMin` > `yMax`. + yMin := 1 + yMax := 0 + argsString := fmt.Sprintf("-a linear -txe 0 1 -tye %d %d -outsize %d %d -ot Byte", yMin, yMax, outXSize, outYSize) + fname := "/vsimem/test.tiff" + gridDs, err := vrtDs.Grid(fname, strings.Split(argsString, " ")) + if err != nil { + // Handles QHull error differently here, as it's a compatibility issue not a gridding error + isQhullError := strings.HasSuffix(err.Error(), "without QHull support") + if isQhullError { + t.Log(`Skipping test, GDAL was built without "Delaunay triangulation" support which is required for the "Linear" gridding algorithm`) + return + } else { + t.Error(err) + return + } + } + defer func() { _ = VSIUnlink(fname) }() + defer gridDs.Close() + originalColors := make([]byte, outXSize*outYSize) + gridDs.Read(0, 0, originalColors, outXSize, outYSize) + + // 2. Put the Dataset generated above, through the `Nearblack` function, to set pixels near BLACK to BLACK + argsNbString := "-near 10 -nb 0" + fname2 := "/vsimem/test1.tiff" + nbDs, err := gridDs.Nearblack(fname2, strings.Split(argsNbString, " ")) + if err != nil { + t.Error(err) + return + } + defer func() { _ = VSIUnlink(fname2) }() + defer nbDs.Close() + nearblackColors := make([]byte, outXSize*outYSize) + nbDs.Read(0, 0, nearblackColors, outXSize, outYSize) + + // 3. Test on all rows that pixels where abs(0 - pixelValue) <= 10, are set to black (0) + for i := 0; i < outYSize; i++ { + startIndex := i * outXSize + assert.Equal(t, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, originalColors[startIndex:startIndex+13]) + assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12}, nearblackColors[startIndex:startIndex+13]) + } +} + +func TestNearblackWhite(t *testing.T) { + // 1. Create an image, linearly interpolated, from white (on the left) to black (on the right), using `Grid()` + var ( + outXSize = 256 + outYSize = 256 + ) + vrtDs, err := CreateVector(Memory, "") + if err != nil { + t.Error(err) + return + } + defer vrtDs.Close() + geom, err := NewGeometryFromWKT("POLYGON((0 0 255, 0 1 255, 1 1 0, 1 0 0))", nil) + if err != nil { + t.Error(err) + return + } + defer geom.Close() + _, err = vrtDs.CreateLayer("grid", nil, GTPolygon) + if err != nil { + t.Error(err) + return + } + _, err = vrtDs.Layers()[0].NewFeature(geom) + if err != nil { + t.Error(err) + return + } + // As of GDAL v3.6, `GDALGrid` will swap `yMin` and `yMax` if `yMin` < `yMax`. In order to make the output of + // earlier GDAL versions (< 3.6) consistent with this, we're setting `yMin` > `yMax`. + yMin := 1 + yMax := 0 + argsString := fmt.Sprintf("-a linear -txe 0 1 -tye %d %d -outsize %d %d -ot Byte", yMin, yMax, outXSize, outYSize) + fname := "/vsimem/test.tiff" + gridDs, err := vrtDs.Grid(fname, strings.Split(argsString, " ")) + if err != nil { + // Handles QHull error differently here, as it's a compatibility issue not a gridding error + isQhullError := strings.HasSuffix(err.Error(), "without QHull support") + if isQhullError { + t.Log(`Skipping test, GDAL was built without "Delaunay triangulation" support which is required for the "Linear" gridding algorithm`) + return + } else { + t.Error(err) + return + } + } + defer func() { _ = VSIUnlink(fname) }() + defer gridDs.Close() + originalColors := make([]byte, outXSize*outYSize) + gridDs.Read(0, 0, originalColors, outXSize, outYSize) + + // 2. Put the Dataset generated above, through the `Nearblack` function, to set pixels near WHITE to WHITE + argsNbString := "-near 10 -nb 0 -white" + fname2 := "/vsimem/test1.tiff" + nbDs, err := gridDs.Nearblack(fname2, strings.Split(argsNbString, " ")) + if err != nil { + t.Error(err) + return + } + defer func() { _ = VSIUnlink(fname2) }() + defer nbDs.Close() + nearblackColors := make([]byte, outXSize*outYSize) + nbDs.Read(0, 0, nearblackColors, outXSize, outYSize) + + // 3. Test on all rows that pixels where abs(255 - pixelValue) <= 10, are set to white (255) + for i := 0; i < outYSize; i++ { + startIndex := i * outXSize + assert.Equal(t, []byte{255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243}, originalColors[startIndex:startIndex+13]) + assert.Equal(t, []byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 244, 243}, nearblackColors[startIndex:startIndex+13]) + } +} + +func TestNearblackInvalidSwitch(t *testing.T) { + // 1. Create a linearly interpolated image that goes from white -> black, using `Grid()` + var ( + outXSize = 256 + outYSize = 256 + ) + fname := "/vsimem/test.tiff" + vrtDs, err := Create(Memory, fname, 1, Byte, outXSize, outYSize) + if err != nil { + t.Error(err) + return + } + defer func() { _ = VSIUnlink(fname) }() + defer vrtDs.Close() + + // 2. Put the Dataset generated above, through the `Nearblack` + fname2 := "/vsimem/test1.tiff" + + _, err = vrtDs.Nearblack(fname2, []string{"-invalidswitch"}) + assert.Error(t, err) + ehc := eh() + _, err = vrtDs.Nearblack(fname2, []string{"-invalidswitch"}, ErrLogger(ehc.ErrorHandler)) + assert.Error(t, err) + + defer func() { _ = VSIUnlink(fname2) }() +} + +func TestNearblackBlackInto(t *testing.T) { + // 1. Create an image, linearly interpolated, from black (on the left) to white (on the right), using `Grid()` + var ( + outXSize = 256 + outYSize = 256 + ) + vrtDs, err := CreateVector(Memory, "") + if err != nil { + t.Error(err) + return + } + defer vrtDs.Close() + geom, err := NewGeometryFromWKT("POLYGON((0 0 0, 0 1 0, 1 1 255, 1 0 255))", nil) + if err != nil { + t.Error(err) + return + } + defer geom.Close() + _, err = vrtDs.CreateLayer("grid", nil, GTPolygon) + if err != nil { + t.Error(err) + return + } + _, err = vrtDs.Layers()[0].NewFeature(geom) + if err != nil { + t.Error(err) + return + } + // As of GDAL v3.6, `GDALGrid` will swap `yMin` and `yMax` if `yMin` < `yMax`. In order to make the output of + // earlier GDAL versions (< 3.6) consistent with this, we're setting `yMin` > `yMax`. + yMin := 1 + yMax := 0 + argsString := fmt.Sprintf("-a linear -txe 0 1 -tye %d %d -outsize %d %d -ot Byte", yMin, yMax, outXSize, outYSize) + fname := "/vsimem/test.tiff" + gridDs, err := vrtDs.Grid(fname, strings.Split(argsString, " ")) + if err != nil { + // Handles QHull error differently here, as it's a compatibility issue not a gridding error + isQhullError := strings.HasSuffix(err.Error(), "without QHull support") + if isQhullError { + t.Log(`Skipping test, GDAL was built without "Delaunay triangulation" support which is required for the "Linear" gridding algorithm`) + return + } else { + t.Error(err) + return + } + } + defer func() { _ = VSIUnlink(fname) }() + defer gridDs.Close() + originalColors := make([]byte, outXSize*outYSize) + gridDs.Read(0, 0, originalColors, outXSize, outYSize) + + // 2. Put the Dataset generated above, through the `Nearblack` function, to set pixels near BLACK to BLACK + nbDs, err := Create(Memory, "nbDs", 1, Byte, outXSize, outYSize) + if err != nil { + t.Error(err) + return + } + defer nbDs.Close() + argsNbString := "-near 10 -nb 0" + err = nbDs.NearblackInto(gridDs, strings.Split(argsNbString, " ")) + if err != nil { + t.Error(err) + return + } + nearblackColors := make([]byte, outXSize*outYSize) + nbDs.Read(0, 0, nearblackColors, outXSize, outYSize) + + // 3. Test on all rows that pixels where abs(0 - pixelValue) <= 10, are set to black (0) + for i := 0; i < outYSize; i++ { + startIndex := i * outXSize + assert.Equal(t, []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, originalColors[startIndex:startIndex+13]) + assert.Equal(t, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12}, nearblackColors[startIndex:startIndex+13]) + } +} + +func TestNearblackIntoInvalidSwitch(t *testing.T) { + // 1. Create a linearly interpolated image that goes from white -> black, using `Grid()` + var ( + outXSize = 256 + outYSize = 256 + ) + fname := "/vsimem/test.tiff" + vrtDs, err := Create(Memory, fname, 1, Byte, outXSize, outYSize) + if err != nil { + t.Error(err) + return + } + defer func() { _ = VSIUnlink(fname) }() + defer vrtDs.Close() + + // 2. Put the Dataset generated above, through the `Nearblack` + nbDs, err := Create(Memory, "nbDs", 1, Byte, outXSize, outYSize) + if err != nil { + t.Error(err) + return + } + defer nbDs.Close() + err = nbDs.NearblackInto(vrtDs, []string{"-invalidswitch"}) + assert.Error(t, err) + ehc := eh() + err = nbDs.NearblackInto(vrtDs, []string{"-invalidswitch"}, ErrLogger(ehc.ErrorHandler)) + assert.Error(t, err) +} + +func TestNearblackIntoNoSrcDs(t *testing.T) { + // 1. Create a linearly interpolated image that goes from white -> black, using `Grid()` + var ( + outXSize = 256 + outYSize = 256 + ) + fname := "/vsimem/test.tiff" + vrtDs, err := Create(Memory, fname, 1, Byte, outXSize, outYSize) + if err != nil { + t.Error(err) + return + } + defer func() { _ = VSIUnlink(fname) }() + defer vrtDs.Close() + + // 2. Put the Dataset generated above, through the `Nearblack` + nbDs, err := Create(Memory, "nbDs", 1, Byte, outXSize, outYSize) + if err != nil { + t.Error(err) + return + } + defer nbDs.Close() + ehc := eh() + err = nbDs.NearblackInto(nil, []string{}, ErrLogger(ehc.ErrorHandler)) + assert.Error(t, err) +} diff --git a/options.go b/options.go index cad7b15..7446308 100644 --- a/options.go +++ b/options.go @@ -1245,6 +1245,15 @@ type GridOption interface { setGridOpt(gOpt *gridOpts) } +type nearBlackOpts struct { + errorHandler ErrorHandler +} + +// NearblackOption is an option that can be passed to Dataset.Nearblack() +type NearblackOption interface { + setNearblackOpt(nbOpt *nearBlackOpts) +} + // RasterizeGeometryOption is an option that can be passed tp Dataset.RasterizeGeometry() type RasterizeGeometryOption interface { setRasterizeGeometryOpt(o *rasterizeGeometryOpts)